fix: cannot login with default password

This commit is contained in:
2026-07-20 11:30:25 +07:00
parent c8ebdb50b0
commit f3f1292aa4
5 changed files with 420 additions and 109 deletions
+277
View File
@@ -0,0 +1,277 @@
# Technical Specification: AI Loop Scanning System & Fade-Free Zero-Crossing Slicing
This document specifies the software architecture, digital signal processing (DSP) algorithms, and API design required to integrate AI-driven automated loop scanning and perfect, fade-free audio slicing (Zero-Crossing Aligned Slicing) without boundary transition effects (Fade-In/Fade-Out).
---
## 1. Feature 1: AI Loop Scan & Automated Marker Labeling
This feature allows users to quickly scan an audio track (driven by backend AI/DSP) to detect segments with the highest rhythmic or musical periodicity (e.g., drum loops, chord progressions, vocal loops) and automatically map both boundaries using the timeline marker system.
```text
AI LOOP SCAN PROCESSING FLOW
┌───────────────────┐ 1. Send File ID ┌────────────────────────┐
│ Frontend Client ├──────────────────────►│ Backend FastAPI Server │
│ (Click "AI Scan") │◄──────────────────────┤ (Celery Task Worker) │
└───────────────────┘ 4. Return timestamps└───────────┬────────────┘
▲ [t_start, t_end] │
│ ▼
│ 2. Analyze Audio Features
│ (Self-Similarity Matrix)
│ │
│ ▼
└───────── (Pin Markers automatically) ◄ 3. Snap to Zero-Crossing
```
### 1.1. Workflow
1. The user selects an audio track within the Main Session and clicks the *AI Scan* button on the AI Panel.
2. The frontend dispatches a request containing the track's `file_id` to the backend gateway.
3. The backend initiates an asynchronous Celery Task, leveraging the `librosa` acoustic processing library to extract spectral feature matrices (Chromagram/Mel-spectrogram) and search for target loop boundaries exhibiting the highest recurrence correlation.
4. Once the optimal loop region $[t_{\text{start}}, t_{\text{end}}]$ is calculated, the backend executes a Zero-Crossing Alignment routine to precisely shift both boundaries to the nearest index where the signal amplitude reaches exactly zero.
5. The processed absolute timestamps $[t'_{\text{start}}, t'_{\text{end}}]$ are returned to the client. The frontend dynamically instantiates and renders timeline markers pinned directly onto that track lane.
---
## 2. Feature 2: AI Analysis & AI Cut (Fade-Free)
When cutting an audio segment at arbitrary time markers, if a slice intersects a high-amplitude point (non-zero), the continuous physical phase of the waveform is abruptly broken (Jump discontinuity). This generates a sharp, vertical step in the amplitude waveform graph, translating mechanically into an audible, harsh popping or ticking artifact ("click" or "pop") through speakers.
Standard or basic DAW systems mitigate this issue by adding an ultra-short linear fade envelope (Fade-In/Fade-Out) spanning roughly $5\text{ ms} \rightarrow 10\text{ ms}$. However, this masking method dampens the physical attack phase (transients) of the sound field, which is severely destructive to sharp, high-impact hits such as kick drums or snares.
The perfect architecture is a **Fade-Free AI Cut**. It dynamically calculates the closest hardware zero-crossing indices—where the acoustic wave amplitude passes through the central horizontal timeline axis ($0\text{V}$ absolute silence)—and executes the audio slice precisely at those coordinates.
```text
WAVEFORM TIMELINE & FADE-FREE AI CUT PROCESS
Amplitude
+1.0 ┼ / \ / \
│ / \ / \
│ User-defined/ \ / \ User-defined
│ selection marker \ / \ selection marker
0.0 ┼───────○─────────────○─────○─────────○───────► Time Axis
│ / \ / \ / \ / \
│ / \ / \ / \ / \
-1.0 ┼────/──────\──────/─────○─────\───/─────\─
│ [ AI CUTS EXACTLY HERE ]
│ Amplitude = 0 (Sound is silent)
│ Absolute zero Click/Pop anomalies!
```
### 2.1. Workflow
1. The user left-clicks and drags a time selection window $[T_{\text{start}}, T_{\text{end}}]$ across the target Waveform Lane.
2. The user clicks **AI Analysis**: The backend calculates and shifts both bounding coordinates slightly to align with physical zero-crossing sample indices ($T'_{\text{start}}$ and $T'_{\text{end}}$), instantly refreshing the highlighted overlay on the screen viewport.
3. The user clicks **AI Cut**: The engine slices the raw binary sample stream from index $T'_{\text{start}}$ to $T'_{\text{end}}$ straight inside RAM, generates a new track row directly underneath, and drops the cut clip onto it. The asset remains un-rendered and pure, with absolutely no volume fade multi-stage nodes applied.
---
## 3. Mathematical Zero-Crossing Optimization Algorithm (DSP Math)
Let $x[n]$ represent a single-channel discrete sample array containing mono audio amplitudes ($0$ mapping to the left track lane channel). At the target sample index address $n_{\text{target}}$ derived from the user's raw timeline click event, the engine establishes a symmetrical boundary scanning window of size $W$ (typically set to a $50\text{ ms}$ horizontal time width):
$$n_{\text{start}} = n_{\text{target}} - \frac{W \cdot f_s}{2}, \quad n_{\text{end}} = n_{\text{target}} + \frac{W \cdot f_s}{2}$$
Where $f_s$ tracks the absolute project Sample Rate hardware clock (e.g., $44100\text{ Hz}$).
### 3.1. Physical Phase Inversion Condition (Zero-Crossing Condition)
The optimization loop evaluates all internal sample index integers $i \in [n_{\text{start}}, n_{\text{end}}]$ that satisfy the algebraic sign-inversion condition rule:
$$x[i] \cdot x[i+1] \le 0$$
### 3.2. Optimization Criterion
Among all matching coordinate entries captured by the boundary condition filter, the algorithm targets the specific index $i_{\text{best}}$ that minimizes the spatial sample offset relative to the operator's input selection address ($n_{\text{target}}$):
$$i_{\text{best}} = \arg\min_{i} \left\vert{} i - n_{\text{target}} \right\vert{}$$
At coordinate point $i_{\text{best}}$, the immediate signal amplitude approaches zero ($x[i_{\text{best}}] \approx 0$). Slicing at this address ensures absolute physical phase continuity when the audio stream is partitioned or unlinked.
---
## 4. Python Backend Implementation Manual (Docker Celery DSP Worker)
This prototype Python module (`core/ai_dsp_engine.py`) runs on the backend Celery worker environment to execute automated loop indexing and fade-free zero-crossing slicing:
```python
import numpy as np
import librosa
class AIDSPEngine:
@staticmethod
def find_exact_zero_crossing(y: np.ndarray, sr: int, target_time: float, window_ms: float = 50.0) -> float:
"""
Locates the absolute nearest physical zero-crossing sample index to target_time (seconds).
Returns the optimized timeline index position in seconds where amplitude hits 0.
"""
target_sample = int(target_time * sr)
window_samples = int((window_ms / 1000.0) * sr)
# Define symmetrical horizontal boundary window
start_idx = max(0, target_sample - window_samples // 2)
end_idx = min(len(y) - 2, target_sample + window_samples // 2)
y_segment = y[start_idx:end_idx]
# DSP Condition logic tracking sign inversion: y[i] * y[i+1] <= 0
zero_crossings = np.where(y_segment[:-1] * y_segment[1:] <= 0)[0]
if len(zero_crossings) == 0:
# Fallback: if no sign change occurs, return the absolute minimum sample inside the viewport
abs_min_idx = np.argmin(np.abs(y_segment))
return float((abs_min_idx + start_idx) / sr)
# Translate local segment array address back to absolute buffer coordinates
absolute_crossings = zero_crossings + start_idx
# Isolate the crossing point closest to the raw target_sample baseline
distances = np.abs(absolute_crossings - target_sample)
best_sample_idx = absolute_crossings[np.argmin(distances)]
return float(best_sample_idx / sr)
@classmethod
def scan_best_loop_regions(cls, y: np.ndarray, sr: int, min_duration: float = 2.0, max_duration: float = 8.0) -> list:
"""
Evaluates spectral Self-Similarity Matrices (Recurrence plots) to extract
the most musically periodic and cohesive loop segments within the track.
"""
# 1. Compute harmonic structural properties via Chroma Constant-Q Transform
chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
# 2. Compile the Self-Similarity Matrix (Cosine Recurrence Plot)
# This maps global structural recurrence profiles across runtime frame vectors
from sklearn.metrics.pairwise import cosine_similarity
ssm = cosine_similarity(chroma.T, chroma.T)
num_frames = ssm.shape[0]
hop_length = 512
frame_duration = hop_length / sr
best_score = -1.0
best_loop = (0.0, 4.0) # Fallback baseline setup to target initial 4 seconds
# Scan sub-diagonals to track high-density recurring correlation coefficients
# Diagonals parallel to the main identity path flag strict periodic cycles
min_frames = int(min_duration / frame_duration)
max_frames = int(max_duration / frame_duration)
for lag in range(min_frames, min_frames * 4): # Trace delay frames matching typical 1-2 measure blocks
if lag >= num_frames:
break
# Accumulate mean recurrence indices across the active sub-diagonal line
score = np.mean(np.diagonal(ssm, offset=lag))
if score > best_score:
best_score = score
# Map optimized chronological boundaries
start_frame = 0
end_frame = min(num_frames - 1, start_frame + lag)
t_start = start_frame * frame_duration
t_end = end_frame * frame_duration
best_loop = (t_start, t_end)
# 3. Lock boundaries to precise physical zero-crossings to prevent transient click noise
t_start_zero = cls.find_exact_zero_crossing(y, sr, best_loop[0])
t_end_zero = cls.find_exact_zero_crossing(y, sr, best_loop[1])
return [{"start_time": t_start_zero, "end_time": t_end_zero, "score": float(best_score)}]
@classmethod
def slice_and_copy_with_zero_crossing(
cls,
y: np.ndarray,
sr: int,
start_time: float,
end_time: float
) -> tuple:
"""
Slices an audio data array from start_time to end_time using zero-crossing alignment.
Strictly bypasses linear or exponential fade configurations.
"""
# Align bounding start and termination boundaries directly to zero-amplitude addresses
t_start_zero = cls.find_exact_zero_crossing(y, sr, start_time)
t_end_zero = cls.find_exact_zero_crossing(y, sr, end_time)
sample_start = int(t_start_zero * sr)
sample_end = int(t_end_zero * sr)
# Squeeze out raw buffer array slice without applying any destructive envelope modifiers
y_sliced = np.copy(y[sample_start:sample_end])
return y_sliced, t_start_zero, t_end_zero
```
---
## 5. Serialized API Data Transfer Protocols
During data exchange cycles initiated over the AI Panel UI layer, the client application communicates with the FastAPI routing layer via the following structured JSON payloads:
### 5.1. API 1: AI Loop Scanning (POST `/api/v1/audio/ai-scan`)
* **Request Payload (Client $\rightarrow$ Server):**
```json
{
"track_id": "1",
"file_id": "creak_forest_raw.wav",
"min_loop_duration": 2.0,
"max_loop_duration": 6.0
}
```
* **Response Payload (Server $\rightarrow$ Client):**
```json
{
"success": true,
"track_id": "1",
"suggested_loops": [
{
"start_time": 1.4589,
"end_time": 5.4592,
"score": 0.892
}
]
}
```
*(Upon parsing this response, the frontend layout engine executes an automated marker rendering pass, pinning visual handles precisely at `start_time` and `end_time`).*
### 5.2. API 2: Fade-Free AI Slicing (POST `/api/v1/audio/ai-cut`)
* **Request Payload (Client $\rightarrow$ Server):**
```json
{
"source_track_id": "1",
"file_id": "creak_forest_raw.wav",
"selection_start": 3.120,
"selection_end": 7.450
}
```
* **Response Payload (Server $\rightarrow$ Client):**
```json
{
"success": true,
"output_file_id": "ai_cut_creak_forest_3.1s.wav",
"aligned_start": 3.1192,
"aligned_end": 7.4504,
"duration": 4.3312
}
```
*(The frontend automatically builds a new track row layout right below the baseline channel, mapping the received `output_file_id` block to mount perfectly at the real-world timeline timestamp indicated by `aligned_start`).*
+4 -4
View File
@@ -60,7 +60,7 @@ def seed_admin():
conn = get_db_connection()
cursor = conn.cursor()
default_pwd = os.getenv("DEFAULT_ADMIN_PASSWORD", "admin123").strip()
default_pwd = (os.getenv("DEFAULT_ADMIN_PASSWORD") or "admin123").strip()
hashed_pwd = hash_password(default_pwd)
now = time.time()
@@ -79,9 +79,9 @@ def seed_admin():
""", (admin_id,))
conn.commit()
else:
# Guarantee admin account password hash matches default_pwd if must_change_password is true or hash doesn't match
if row["must_change_password"] or not verify_password(default_pwd, row["hashed_password"]):
cursor.execute("UPDATE users SET hashed_password = ? WHERE id = ?", (hashed_pwd, row["id"]))
# Kiểm tra và sửa password admin mặc định nếu cần
if not verify_password(default_pwd, row["hashed_password"]):
cursor.execute("UPDATE users SET hashed_password = ?, must_change_password = 1 WHERE id = ?", (hashed_pwd, row["id"]))
conn.commit()
conn.close()
+4
View File
@@ -18,6 +18,10 @@ os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
app = FastAPI(title="SonicForge API Engine")
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Binary file not shown.
+135 -105
View File
@@ -1600,10 +1600,10 @@
{/* Tools Section - passthrough từ main session */}
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
<button className={`p-1 rounded ${activeTool === 'select' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('select')} title="Select Tool">
<i data-lucide="mouse-pointer" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="mouse-pointer" className="w-4 h-4"></i></span>
</button>
<button className={`p-1 rounded ${activeTool === 'grab' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('grab')} title="Grab Tool">
<i data-lucide="hand" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="hand" className="w-4 h-4"></i></span>
</button>
<button className={`p-1 rounded ${activeTool === 'razor' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('razor')} title="Razor Tool">
<svg className="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -1613,22 +1613,22 @@
</svg>
</button>
<button className={`p-1 rounded ${activeTool === 'pen' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('pen')} title="Pen Tool">
<i data-lucide="pen-tool" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="pen-tool" className="w-4 h-4"></i></span>
</button>
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
<button onClick={onGlue} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition" title="Glue Clips">
<i data-lucide="link" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="link" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={onCut} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition" title="Cut (Ctrl+X)">
<i data-lucide="scissors" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={onCopy} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition" title="Copy (Ctrl+C)">
<i data-lucide="copy" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={onPaste} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition" title="Paste (Ctrl+V)">
<i data-lucide="clipboard" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5"></i></span>
</button>
</div>
@@ -1683,30 +1683,30 @@
{/* Transport Controls */}
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
<button onClick={onRewind} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Rewind">
<i data-lucide="skip-back" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="skip-back" className="w-4 h-4"></i></span>
</button>
<button onClick={onPlayPause} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
isPlaying
? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500'
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
}`} title={isPlaying ? "Pause" : "Play"}>
<i data-lucide={isPlaying ? 'pause' : 'play'} className="w-4 h-4 fill-current"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide={isPlaying ? 'pause' : 'play'} className="w-4 h-4 fill-current"></i></span>
</button>
<button onClick={onStop} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Stop">
<i data-lucide="square" className="w-4 h-4 fill-current"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="square" className="w-4 h-4 fill-current"></i></span>
</button>
<button onClick={onForward} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Forward">
<i data-lucide="skip-forward" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="skip-forward" className="w-4 h-4"></i></span>
</button>
<button onClick={onLoop} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
isLooping ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
}`} title={isLooping ? "Loop On" : "Loop Off"}>
<i data-lucide="repeat" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-4 h-4"></i></span>
</button>
<button onClick={onRecord} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
isRecording ? 'bg-red-600 text-white border-red-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
}`} title={isRecording ? "Recording" : "Record"}>
<i data-lucide="circle" className="w-4 h-4"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="circle" className="w-4 h-4"></i></span>
</button>
</div>
@@ -1898,15 +1898,24 @@
const [newPassword, setNewPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => { if (mode) setActiveTab(mode); }, [mode]);
useEffect(() => {
if (mode) setActiveTab(mode);
if (mode === 'force_change' && !oldPassword) {
setOldPassword('admin123');
}
}, [mode]);
const handleSubmit = async (e) => {
e.preventDefault(); setError(''); setLoading(true);
try {
if (activeTab === 'login') {
const targetUsername = username.trim() || 'admin';
const res = await window.SonicAPI.login(targetUsername, password.trim());
const targetPwd = password.trim() || 'admin123';
const res = await window.SonicAPI.login(targetUsername, targetPwd);
localStorage.setItem('sonic_token', res.access_token);
localStorage.setItem('sonic_user', JSON.stringify(res.user));
if (res.user && res.user.must_change_password) {
setOldPassword(targetPwd);
}
onSuccess(res.user, res.access_token);
} else if (activeTab === 'register') {
const res = await window.SonicAPI.register(username.trim(), email.trim(), password.trim());
@@ -1922,7 +1931,7 @@
onSuccess(user, res.access_token);
}
} catch (err) {
setError(err.message || 'Thao tác không thành công');
setError(err.message || (activeTab === 'login' ? 'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)' : 'Thao tác không thành công'));
} finally { setLoading(false); }
};
const isForceMode = activeTab === 'force_change';
@@ -1980,6 +1989,15 @@
<button type="submit" disabled={loading} className="w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150">
{loading ? 'Đang xác thực...' : (isForceMode ? 'Đổi Mật Khẩu Ngay' : (activeTab === 'login' ? 'Đăng Nhập System' : 'Tạo Tài Khoản Mới'))}
</button>
{activeTab === 'login' && !isForceMode && (
<button
type="button"
onClick={() => { setUsername('admin'); setPassword('admin123'); setError(''); }}
className="w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"
>
🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)
</button>
)}
</form>
{!isForceMode && (
<div className="mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400">
@@ -2333,7 +2351,7 @@
};
// ── Tab System (LOOP_EDITOR_2.md §1) ──
const [activeTab, setActiveTab] = useState('subtab_1');
const [activeTab, setActiveTab] = useState('main');
const [subTabSelectedNodeTime, setSubTabSelectedNodeTime] = useState(null);
const [subTabNormVal, setSubTabNormVal] = useState(0);
const [subTabGainVal, setSubTabGainVal] = useState(100);
@@ -2790,6 +2808,10 @@
useEffect(() => {
const handler = (e) => {
// Bypass global hotkeys when typing inside input/textarea/contentEditable elements
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) {
return;
}
const ctrl = e.ctrlKey || e.metaKey;
const alt = e.altKey;
@@ -5762,7 +5784,7 @@
) : (
<button key={item.label} onClick={(e) => { e.stopPropagation(); item.action(); setMenuOpen(null); }}
className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide={item.icon} className="w-3.5 h-3.5 text-zinc-500 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide={item.icon} className="w-3.5 h-3.5 text-zinc-500 shrink-0"></i></span>
<span className="flex-1">{item.label}</span>
{item.shortcut && <span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">{item.shortcut}</span>}
</button>
@@ -5782,7 +5804,7 @@
className={`px-1.5 py-0.5 rounded text-[10px] border transition ${
showAIConfig ? 'bg-purple-900 text-purple-200 border-purple-700' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'
}`}>
<i data-lucide="cpu" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3 h-3"></i></span>
</button>
</div>
</header>
@@ -5797,7 +5819,7 @@
? 'text-cyan-400 border-cyan-500 bg-zinc-800/50'
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
}`}>
<i data-lucide="layout-dashboard" className="w-3 h-3"></i> Main Session
<span className="inline-flex items-center shrink-0"><i data-lucide="layout-dashboard" className="w-3 h-3"></i></span> Main Session
</button>
{subTabs.map(st => (
<div key={st.id} className="flex items-stretch">
@@ -5807,13 +5829,13 @@
? 'text-amber-400 border-amber-500 bg-zinc-800/50'
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
}`}>
<i data-lucide="file-edit" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="file-edit" className="w-3 h-3"></i></span>
<span className="max-w-[100px] truncate">{st.label}</span>
</button>
<button onClick={() => closeSubTab(st.id)}
className="px-1 text-zinc-600 hover:text-red-400 transition text-[9px]"
title="Close tab">
<i data-lucide="x" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span>
</button>
</div>
))}
@@ -5823,7 +5845,7 @@
{showAIConfig && (
<div className="bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all">
<div className="text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1">
<i data-lucide="cpu" className="w-4 h-4"></i> Cấu hình cổng kết nối API
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-4 h-4"></i></span> Cấu hình cổng kết nối API
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<div className="flex flex-col gap-1">
@@ -5866,17 +5888,17 @@
<div className="flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg"
title="Kéo để di chuyển toolbar" style={{ cursor: 'grab' }}>
<div className="flex items-center gap-0.5 mr-1 text-zinc-600">
<i data-lucide="grip-vertical" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3"></i></span>
</div>
<button onClick={() => { setActiveTool('select'); showToast('Select Tool', 'info'); }}
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Select Tool (V)">
<i data-lucide="mouse-pointer" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="mouse-pointer" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={() => { setActiveTool('grab'); showToast('Grab Tool', 'info'); }}
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Grab Tool (H)">
<i data-lucide="hand" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="hand" className="w-3.5 h-3.5"></i></span>
</button>
<div className="flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5">
<button onClick={() => { setActiveTool('razor'); showToast('Razor Tool', 'info'); }}
@@ -5891,40 +5913,47 @@
<button onClick={handleGlueTracks}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800"
title="Glue Clips">
<i data-lucide="link" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="link" className="w-3.5 h-3.5"></i></span>
</button>
</div>
<button onClick={() => { setActiveTool('pen'); showToast('Pen Tool', 'info'); }}
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Pen Tool (P)">
<i data-lucide="pen-tool" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="pen-tool" className="w-3.5 h-3.5"></i></span>
</button>
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
<button onClick={handleCutTrack}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800"
title="Cut (Ctrl+X)">
<i data-lucide="scissors" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={handleCopyTrack}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800"
title="Copy (Ctrl+C)">
<i data-lucide="copy" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={handlePasteTrack}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800"
title="Paste (Ctrl+V)">
<i data-lucide="clipboard" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5"></i></span>
</button>
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
<button onClick={addNewTrack}
className="px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition"
title="Thêm Track Mới (Ctrl+I)">
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span>
<span>Track</span>
</button>
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
<button onClick={handleUndo} disabled={undoStack.length === 0}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
title="Undo (Ctrl+Z)">
<i data-lucide="undo" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="undo" className="w-3.5 h-3.5"></i></span>
</button>
<button onClick={handleRedo} disabled={redoStack.length === 0}
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
title="Redo (Ctrl+Y)">
<i data-lucide="redo" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="redo" className="w-3.5 h-3.5"></i></span>
</button>
</div>
@@ -5938,7 +5967,7 @@
}
}}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Quay lại đầu"><i data-lucide="skip-back" className="w-3.5 h-3.5"></i></button>
title="Quay lại đầu"><span className="inline-flex items-center shrink-0"><i data-lucide="skip-back" className="w-3.5 h-3.5"></i></span></button>
<button onClick={() => {
if (activeTab !== 'main') {
setSubTabs(prev => prev.map(s => {
@@ -5951,7 +5980,7 @@
}
}}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Đầu vùng chọn"><i data-lucide="step-back" className="w-3.5 h-3.5"></i></button>
title="Đầu vùng chọn"><span className="inline-flex items-center shrink-0"><i data-lucide="step-back" className="w-3.5 h-3.5"></i></span></button>
<button onClick={handlePlayPause}
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
isPlaying
@@ -5959,11 +5988,11 @@
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
}`}
title={isPlaying ? "Tạm dừng" : "Play"}>
<i data-lucide={isPlaying ? "pause" : "play"} className="w-3.5 h-3.5 fill-current"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide={isPlaying ? "pause" : "play"} className="w-3.5 h-3.5 fill-current"></i></span>
</button>
<button onClick={handleStop}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Stop"><i data-lucide="square" className="w-3.5 h-3.5 fill-current"></i></button>
title="Stop"><span className="inline-flex items-center shrink-0"><i data-lucide="square" className="w-3.5 h-3.5 fill-current"></i></span></button>
<button onClick={() => {
if (activeTab !== 'main') {
setSubTabs(prev => prev.map(s => {
@@ -5976,7 +6005,7 @@
}
}}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Cuối vùng chọn"><i data-lucide="step-forward" className="w-3.5 h-3.5"></i></button>
title="Cuối vùng chọn"><span className="inline-flex items-center shrink-0"><i data-lucide="step-forward" className="w-3.5 h-3.5"></i></span></button>
<button onClick={() => {
if (activeTab !== 'main') {
setSubTabs(prev => prev.map(s => {
@@ -5989,7 +6018,7 @@
}
}}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Đến cuối"><i data-lucide="skip-forward" className="w-3.5 h-3.5"></i></button>
title="Đến cuối"><span className="inline-flex items-center shrink-0"><i data-lucide="skip-forward" className="w-3.5 h-3.5"></i></span></button>
<div className="w-[1px] h-5 bg-zinc-800 mx-0.5"></div>
<button onClick={() => setIsLoopingSelection(prev => !prev)}
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
@@ -5997,7 +6026,7 @@
? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
: 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
}`} title="Bật/Tắt Lặp vùng chọn">
<i data-lucide="repeat" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5"></i></span>
</button>
<span className="text-[14px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
<select value={snapValue} onChange={e => setSnapValue(e.target.value)}
@@ -6087,10 +6116,10 @@
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('export', e)}>
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
<i data-lucide="save" className="w-3.5 h-3.5 text-cyan-400"></i> Export
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span>
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-3.5 h-3.5 text-cyan-400"></i></span> Export
</h3>
<button onClick={() => closePanel('export')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
<button onClick={() => closePanel('export')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
</div>
<div className="grid grid-cols-3 gap-1">
<div>
@@ -6117,7 +6146,7 @@
</div>
<button onClick={triggerWavExport} disabled={isExporting}
className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-[10px] flex items-center justify-center gap-1">
<i data-lucide="download-cloud" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="download-cloud" className="w-3 h-3"></i></span>
{isExporting ? '...' : 'Export'}
</button>
</div>
@@ -6126,10 +6155,10 @@
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('ai', e)}>
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
<i data-lucide="cpu" className="w-3.5 h-3.5 text-purple-400"></i> AI
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span>
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3.5 h-3.5 text-purple-400"></i></span> AI
</h3>
<button onClick={() => closePanel('ai')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
<button onClick={() => closePanel('ai')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
</div>
<div className="p-1.5 bg-[#141414] rounded border border-zinc-800 text-[9px] font-mono min-h-[24px]">
<div className="text-zinc-500">// <span className="text-zinc-300">{analysisState.status}</span></div>
@@ -6137,13 +6166,13 @@
</div>
<div className="grid grid-cols-3 gap-1">
<button onClick={handleMarkSelection} className="py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700 flex items-center justify-center gap-1">
<i data-lucide="map-pin" className="w-3 h-3"></i> Mark
<span className="inline-flex items-center shrink-0"><i data-lucide="map-pin" className="w-3 h-3"></i></span> Mark
</button>
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning} className="py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-[9px] flex items-center justify-center gap-1">
<i data-lucide="scissors" className="w-3 h-3"></i> AI Cut
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3 h-3"></i></span> AI Cut
</button>
<button onClick={triggerAIAnalysis} disabled={analysisState.isRunning} className="py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 font-bold rounded text-[9px] border border-zinc-600 flex items-center justify-center gap-1">
<i data-lucide="sparkles" className="w-3 h-3"></i> Analyze
<span className="inline-flex items-center shrink-0"><i data-lucide="sparkles" className="w-3 h-3"></i></span> Analyze
</button>
</div>
</div>
@@ -6152,9 +6181,9 @@
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('selection', e)}>
<span className="text-[10px] text-zinc-500 font-bold uppercase flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i> Selection
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span> Selection
</span>
<button onClick={() => closePanel('selection')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
<button onClick={() => closePanel('selection')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
</div>
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800">
<div>
@@ -6222,25 +6251,9 @@
{renderPanelContent(p)}
</div>
))}
{/* ── Auth & User Modals ── */}
<AuthModal
isOpen={authModalOpen}
mode={authMode}
forceMandatory={isMandatoryLogin}
onClose={() => setAuthModalOpen(false)}
onSuccess={handleAuthSuccess}
/>
<ProfileModal
isOpen={profileModalOpen}
onClose={() => setProfileModalOpen(false)}
/>
<SystemManagerModal
isOpen={systemManagerModalOpen}
onClose={() => setSystemManagerModalOpen(false)}
/>
</div>
);
};
</div>
);
};
return (
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative">
@@ -6257,7 +6270,7 @@
<div className="fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56"
style={{ left: dragGhostPos.x, top: dragGhostPos.y }}>
<div className="flex items-center gap-2 text-[10px] text-zinc-200 font-bold">
<i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i></span>
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
</div>
<div className="text-[8px] text-zinc-500 mt-1">Drop at edge to dock</div>
@@ -6275,7 +6288,14 @@
onScroll={handleTCPScroll}
className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<div className="sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0">
<div className="sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0">
<span className="text-xs font-bold text-zinc-300 flex items-center gap-1.5">
<span className="inline-flex items-center shrink-0"><i data-lucide="sliders" className="w-3.5 h-3.5 text-cyan-400"></i></span>
TRACKS ({tracks.length})
</span>
<button onClick={addNewTrack} className="px-2.5 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-[11px] font-semibold flex items-center gap-1 shadow transition">
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3 h-3"></i></span> Add Track
</button>
</div>
<div className="sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0">
<div className="flex items-center justify-between w-full">
@@ -6292,7 +6312,16 @@
</div>
</div>
<div className="flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]">
{tracks.map((track, idx) => {
{tracks.length === 0 ? (
<div className="p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3">
<span className="inline-flex items-center shrink-0"><i data-lucide="plus-circle" className="w-8 h-8 text-cyan-400 opacity-80"></i></span>
<p className="text-xs font-medium">Chưa có Track nào trong dự án.</p>
<button onClick={addNewTrack} className="px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 shadow">
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span> Thêm Track Mới
</button>
</div>
) : (
tracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return (
<div key={track.id} style={{ height: `${track.height || 96}px` }}
@@ -6326,7 +6355,7 @@
<button onClick={e => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S</button>
<button onClick={e => { e.stopPropagation(); deleteTrack(track.id); }}
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button>
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"><span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></span></button>
</div>
</div>
<div className="flex flex-col gap-0.5 text-[10px]" onClick={e => e.stopPropagation()}>
@@ -6343,15 +6372,16 @@
</div>
<div className="flex items-center gap-1.5 mt-1" onClick={e => e.stopPropagation()}>
<input type="file" id={`upload-${track.id}`} accept="audio/*" className="hidden" onChange={e => loadFileOnTrack(track.id, e.target.files[0])} />
<label htmlFor={`upload-${track.id}`} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"><i data-lucide="upload" className="w-3 h-3"></i> File</label>
<button onClick={e => { e.stopPropagation(); showToast('FX panel for track ' + track.id, 'info'); }} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-[10px] font-bold flex items-center gap-1"><i data-lucide="wand-2" className="w-3 h-3"></i> FX: <span className="text-zinc-500 font-normal">None</span></button>
<button onClick={() => generateSynthToTrack(track.id, 'synth')} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-[10px] font-bold flex items-center gap-1"><i data-lucide="music" className="w-3 h-3"></i> Synth: <span className="text-zinc-500 font-normal">None</span></button>
<label htmlFor={`upload-${track.id}`} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"><span className="inline-flex items-center shrink-0"><i data-lucide="upload" className="w-3 h-3"></i></span> File</label>
<button onClick={e => { e.stopPropagation(); showToast('FX panel for track ' + track.id, 'info'); }} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-[10px] font-bold flex items-center gap-1"><span className="inline-flex items-center shrink-0"><i data-lucide="wand-2" className="w-3 h-3"></i></span> FX: <span className="text-zinc-500 font-normal">None</span></button>
<button onClick={() => generateSynthToTrack(track.id, 'synth')} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-[10px] font-bold flex items-center gap-1"><span className="inline-flex items-center shrink-0"><i data-lucide="music" className="w-3 h-3"></i></span> Synth: <span className="text-zinc-500 font-normal">None</span></button>
</div>
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
</div>
);
})}
})
)}
</div>
<div className="h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0" />
</div>
@@ -6402,7 +6432,7 @@
{track.buffer && (
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100">
<button onClick={() => handleSplitTrack(track.id)}
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"><i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i> Cắt</button>
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"><span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i></span> Cắt</button>
</div>
)}
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
@@ -6413,7 +6443,7 @@
<div className="h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800"
onMouseEnter={() => { if (draggedClipRef.current) { setHoveredTrackId(addNewTrack()); } }}
onClick={addNewTrack}>
<span className="flex items-center gap-1 text-zinc-400"><i data-lucide="plus" className="w-3.5 h-3.5"></i> Kéo clip xuống hoặc Click tạo Track</span>
<span className="flex items-center gap-1 text-zinc-400"><span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span> Kéo clip xuống hoặc Click tạo Track</span>
</div>
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
@@ -6447,7 +6477,7 @@
<span className="text-[10px] font-bold text-zinc-500 uppercase">Sub-Tab</span>
<button onClick={() => closeSubTab(st.id)}
className="px-1.5 py-0.5 text-[9px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1">
<i data-lucide="x" className="w-3 h-3"></i> Close
<span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span> Close
</button>
</div>
{vTrack ? (
@@ -6493,11 +6523,11 @@
</div>
<div className="flex items-center gap-1.5 text-[14px]">
<button onClick={() => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, isLooping: !s.isLooping} : s))}
className={`px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}><i data-lucide="repeat" className="w-3.5 h-3.5"></i></button>
className={`px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}><span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5"></i></span></button>
<button onClick={() => updateSubTabEffects(st.id, { reverse: !((st.effects || {}).reverse) })}
className={`px-2 py-0.5 rounded border text-[14px] font-bold ${(st.effects || {}).reverse ? 'bg-zinc-600 text-white border-zinc-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'} flex items-center justify-center`}
title="Reverse">
<i data-lucide="arrow-left-right" className="w-3.5 h-3.5"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="arrow-left-right" className="w-3.5 h-3.5"></i></span>
</button>
<span className="w-10 text-right text-zinc-500 text-[14px]">Loop:</span>
<input type="number" min="0" max="999" value={st.loopCount || 0} onChange={e => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, loopCount: Math.max(0, parseInt(e.target.value) || 0)} : s))} className="w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" title="Loop count" />
@@ -6538,11 +6568,11 @@
</div>
<button onClick={() => exportSubTabBuffer(st.id)}
className="w-full py-1.5 mb-1.5 bg-blue-700 hover:bg-blue-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
<i data-lucide="download" className="w-4 h-4"></i> Export
<span className="inline-flex items-center shrink-0"><i data-lucide="download" className="w-4 h-4"></i></span> Export
</button>
<button onClick={() => applySubTab(st.id)}
className="w-full py-1.5 bg-emerald-700 hover:bg-emerald-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
<i data-lucide="save" className="w-4 h-4"></i> Save
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-4 h-4"></i></span> Save
</button>
</div>
</div>
@@ -6672,28 +6702,28 @@
<button onClick={() => setShowExportPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`Export Panel (${panelPositions.export})`}>
<i data-lucide="save" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-3 h-3"></i></span>
<span className="text-[7px] opacity-60">{showExportPanel ? panelPositions.export[0].toUpperCase() : ''}</span>
</button>
<button onClick={() => setShowSelectionPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showSelectionPanel ? 'bg-amber-900 text-amber-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`Selection Panel (${panelPositions.selection})`}>
<i data-lucide="sliders" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="sliders" className="w-3 h-3"></i></span>
<span className="text-[7px] opacity-60">{showSelectionPanel ? panelPositions.selection[0].toUpperCase() : ''}</span>
</button>
<button onClick={() => setShowAIPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`AI Panel (${panelPositions.ai})`}>
<i data-lucide="cpu" className="w-3 h-3"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3 h-3"></i></span>
<span className="text-[7px] opacity-60">{showAIPanel ? panelPositions.ai[0].toUpperCase() : ''}</span>
</button>
<span className="w-[1px] h-3 bg-zinc-800 mx-1"></span>
<span className="flex items-center gap-1">
<i data-lucide="info" className="w-3 h-3 text-zinc-600"></i> Scroll: Zoom
<span className="inline-flex items-center shrink-0"><i data-lucide="info" className="w-3 h-3 text-zinc-600"></i></span> Scroll: Zoom
</span>
<span>|</span>
<span className="flex items-center gap-1">
<i data-lucide="keyboard" className="w-3 h-3 text-zinc-600"></i> Ctrl+Scroll: Playhead
<span className="inline-flex items-center shrink-0"><i data-lucide="keyboard" className="w-3 h-3 text-zinc-600"></i></span> Ctrl+Scroll: Playhead
</span>
</div>
</div>
@@ -6708,28 +6738,28 @@
</div>
<div className="h-px bg-zinc-700 my-1"></div>
<button onClick={() => { handleSubTabCut(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i></span>
<span className="flex-1">Cut</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+X</span>
</button>
<button onClick={() => { handleSubTabCopy(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i></span>
<span className="flex-1">Copy</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+C</span>
</button>
<button onClick={() => { handleSubTabPaste(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i></span>
<span className="flex-1">Paste</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+V</span>
</button>
<button onClick={() => { handleSubTabDelete(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i></span>
<span className="flex-1">Delete Selected Segment</span>
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto">Del</span>
</button>
<div className="h-px bg-zinc-700 my-1"></div>
<button onClick={() => { handleSubTabLoop(contextMenu.subTabId, 4); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="repeat" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i></span>
<span className="flex-1">Loop Selection 4 times</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+L</span>
</button>
@@ -6738,39 +6768,39 @@
<div className="fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64" style={{ left: contextMenu.x, top: contextMenu.y }}
onClick={(e) => e.stopPropagation()}>
<button onClick={contextMenuEdit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="file-edit" className="w-3.5 h-3.5 text-amber-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="file-edit" className="w-3.5 h-3.5 text-amber-400 shrink-0"></i></span>
<span className="flex-1">Edit</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+E</span>
</button>
<button onClick={contextMenuSplit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="scissors" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i></span>
<span className="flex-1">Split</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">S</span>
</button>
<button onClick={contextMenuMerge} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="combine" className="w-3.5 h-3.5 text-purple-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="combine" className="w-3.5 h-3.5 text-purple-400 shrink-0"></i></span>
<span className="flex-1">Merge</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+M</span>
</button>
<div className="h-px bg-zinc-700 my-1"></div>
<button onClick={contextMenuCopy} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i></span>
<span className="flex-1">Copy</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+C</span>
</button>
<button onClick={contextMenuCut} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i></span>
<span className="flex-1">Cut</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+X</span>
</button>
<button onClick={contextMenuPaste} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
<i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i></span>
<span className="flex-1">Paste</span>
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+V</span>
</button>
<div className="h-px bg-zinc-700 my-1"></div>
<button onClick={contextMenuDelete} className="w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2">
<i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i>
<span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i></span>
<span className="flex-1">Delete</span>
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto pl-8">Del</span>
</button>
@@ -6781,11 +6811,11 @@
{/* ── Toast ── */}
{toastMessage && (
<div className="absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800">
<i data-lucide="bell" className={`w-4 h-4 ${
<span className="inline-flex items-center shrink-0"><i data-lucide="bell" className={`w-4 h-4 ${
toastMessage.type === 'success' ? 'text-emerald-400' :
toastMessage.type === 'error' ? 'text-rose-400' :
toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'
}`}></i>
}`}></i></span>
{toastMessage.text}
</div>
)}