@assistkick/create 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
|
@@ -7,18 +7,33 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
7
7
|
import { apiClient } from '../api/client';
|
|
8
8
|
import { useProjectStore } from '../stores/useProjectStore';
|
|
9
9
|
import { Button } from './ds/Button';
|
|
10
|
-
import { Plus, RotateCcw, Trash2, Save, ChevronDown } from 'lucide-react';
|
|
10
|
+
import { Plus, RotateCcw, Trash2, Save, ChevronDown, X } from 'lucide-react';
|
|
11
11
|
|
|
12
12
|
interface Agent {
|
|
13
13
|
id: string;
|
|
14
14
|
name: string;
|
|
15
|
-
|
|
15
|
+
model: string;
|
|
16
|
+
skills: string;
|
|
17
|
+
grounding: string;
|
|
18
|
+
defaultGrounding: string | null;
|
|
16
19
|
projectId: string | null;
|
|
17
20
|
isDefault: number;
|
|
18
21
|
createdAt: string;
|
|
19
22
|
updatedAt: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
interface AvailableSkill {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
preview: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AVAILABLE_MODELS = [
|
|
32
|
+
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
|
33
|
+
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
|
34
|
+
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
|
|
35
|
+
];
|
|
36
|
+
|
|
22
37
|
const PLACEHOLDER_DOCS = [
|
|
23
38
|
{ name: '{{featureId}}', description: 'The feature ID being worked on', stages: 'All' },
|
|
24
39
|
{ name: '{{projectId}}', description: 'The current project ID', stages: 'All' },
|
|
@@ -40,7 +55,11 @@ export function AgentsView() {
|
|
|
40
55
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
41
56
|
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
|
42
57
|
const [editName, setEditName] = useState('');
|
|
43
|
-
const [
|
|
58
|
+
const [editModel, setEditModel] = useState('claude-opus-4-6');
|
|
59
|
+
const [editSkills, setEditSkills] = useState<string[]>([]);
|
|
60
|
+
const [editGrounding, setEditGrounding] = useState('');
|
|
61
|
+
const [availableSkills, setAvailableSkills] = useState<AvailableSkill[]>([]);
|
|
62
|
+
const [skillPickerOpen, setSkillPickerOpen] = useState(false);
|
|
44
63
|
const [loading, setLoading] = useState(true);
|
|
45
64
|
const [saving, setSaving] = useState(false);
|
|
46
65
|
const [error, setError] = useState('');
|
|
@@ -60,14 +79,36 @@ export function AgentsView() {
|
|
|
60
79
|
}
|
|
61
80
|
}, [scope, projectId]);
|
|
62
81
|
|
|
82
|
+
const fetchSkills = useCallback(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const resp = await apiClient.fetchAvailableSkills();
|
|
85
|
+
setAvailableSkills(resp.skills);
|
|
86
|
+
} catch {
|
|
87
|
+
// Skills list is non-critical; silently fall back to empty
|
|
88
|
+
}
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
63
91
|
useEffect(() => {
|
|
64
92
|
fetchData();
|
|
65
93
|
}, [fetchData]);
|
|
66
94
|
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
fetchSkills();
|
|
97
|
+
}, [fetchSkills]);
|
|
98
|
+
|
|
67
99
|
useEffect(() => {
|
|
68
100
|
if (selectedProjectId) setProjectId(selectedProjectId);
|
|
69
101
|
}, [selectedProjectId]);
|
|
70
102
|
|
|
103
|
+
const parseSkills = (skillsJson: string): string[] => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(skillsJson);
|
|
106
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
71
112
|
const showSuccess = (msg: string) => {
|
|
72
113
|
setSuccessMsg(msg);
|
|
73
114
|
setTimeout(() => setSuccessMsg(''), 3000);
|
|
@@ -76,15 +117,20 @@ export function AgentsView() {
|
|
|
76
117
|
const selectAgent = (agent: Agent) => {
|
|
77
118
|
setSelectedAgent(agent);
|
|
78
119
|
setEditName(agent.name);
|
|
79
|
-
|
|
120
|
+
setEditModel(agent.model);
|
|
121
|
+
setEditSkills(parseSkills(agent.skills));
|
|
122
|
+
setEditGrounding(agent.grounding);
|
|
80
123
|
setError('');
|
|
124
|
+
setSkillPickerOpen(false);
|
|
81
125
|
};
|
|
82
126
|
|
|
83
127
|
const handleCreate = async () => {
|
|
84
128
|
try {
|
|
85
129
|
const data: any = {
|
|
86
130
|
name: 'New Agent',
|
|
87
|
-
|
|
131
|
+
model: 'claude-opus-4-6',
|
|
132
|
+
skills: '[]',
|
|
133
|
+
grounding: 'You are a helpful agent.\n\nFeature: {{featureId}}\nProject: {{projectId}}',
|
|
88
134
|
};
|
|
89
135
|
if (scope === 'project' && projectId) data.projectId = projectId;
|
|
90
136
|
const resp = await apiClient.createAgent(data);
|
|
@@ -96,14 +142,27 @@ export function AgentsView() {
|
|
|
96
142
|
}
|
|
97
143
|
};
|
|
98
144
|
|
|
145
|
+
const validateForm = (): string | null => {
|
|
146
|
+
if (!editModel) return 'Model is required';
|
|
147
|
+
if (!editGrounding.trim()) return 'Grounding template cannot be empty';
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
|
|
99
151
|
const handleSave = async () => {
|
|
100
152
|
if (!selectedAgent) return;
|
|
153
|
+
const validationError = validateForm();
|
|
154
|
+
if (validationError) {
|
|
155
|
+
setError(validationError);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
101
158
|
setSaving(true);
|
|
102
159
|
setError('');
|
|
103
160
|
try {
|
|
104
161
|
const resp = await apiClient.updateAgent(selectedAgent.id, {
|
|
105
162
|
name: editName.trim(),
|
|
106
|
-
|
|
163
|
+
model: editModel,
|
|
164
|
+
skills: JSON.stringify(editSkills),
|
|
165
|
+
grounding: editGrounding,
|
|
107
166
|
});
|
|
108
167
|
setSelectedAgent(resp.agent);
|
|
109
168
|
await fetchData();
|
|
@@ -141,7 +200,27 @@ export function AgentsView() {
|
|
|
141
200
|
}
|
|
142
201
|
};
|
|
143
202
|
|
|
144
|
-
const
|
|
203
|
+
const toggleSkill = (skillId: string) => {
|
|
204
|
+
setEditSkills((prev) =>
|
|
205
|
+
prev.includes(skillId) ? prev.filter((s) => s !== skillId) : [...prev, skillId],
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const removeSkill = (skillId: string) => {
|
|
210
|
+
setEditSkills((prev) => prev.filter((s) => s !== skillId));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const hasChanges =
|
|
214
|
+
selectedAgent &&
|
|
215
|
+
(editName !== selectedAgent.name ||
|
|
216
|
+
editModel !== selectedAgent.model ||
|
|
217
|
+
JSON.stringify(editSkills) !== JSON.stringify(parseSkills(selectedAgent.skills)) ||
|
|
218
|
+
editGrounding !== selectedAgent.grounding);
|
|
219
|
+
|
|
220
|
+
const groundingModified =
|
|
221
|
+
selectedAgent?.isDefault === 1 &&
|
|
222
|
+
selectedAgent.defaultGrounding !== null &&
|
|
223
|
+
editGrounding !== selectedAgent.defaultGrounding;
|
|
145
224
|
|
|
146
225
|
return (
|
|
147
226
|
<div className="flex h-full">
|
|
@@ -272,15 +351,116 @@ export function AgentsView() {
|
|
|
272
351
|
</div>
|
|
273
352
|
</div>
|
|
274
353
|
|
|
275
|
-
{/*
|
|
354
|
+
{/* Model & Skills Row */}
|
|
355
|
+
<div className="px-4 pt-4 pb-2 space-y-3 border-b border-edge">
|
|
356
|
+
{/* Model Selector */}
|
|
357
|
+
<div>
|
|
358
|
+
<div className="text-[11px] font-medium text-content-muted uppercase tracking-wider mb-1.5">Model</div>
|
|
359
|
+
<div className="relative">
|
|
360
|
+
<select
|
|
361
|
+
value={editModel}
|
|
362
|
+
onChange={(e) => setEditModel(e.target.value)}
|
|
363
|
+
className="w-full px-3 py-1.5 rounded-md border border-edge bg-surface text-[12px] text-content appearance-none pr-7"
|
|
364
|
+
>
|
|
365
|
+
{AVAILABLE_MODELS.map(m => (
|
|
366
|
+
<option key={m.value} value={m.value}>{m.label}</option>
|
|
367
|
+
))}
|
|
368
|
+
</select>
|
|
369
|
+
<ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-content-muted pointer-events-none" />
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{/* Skills Picker */}
|
|
374
|
+
<div>
|
|
375
|
+
<div className="text-[11px] font-medium text-content-muted uppercase tracking-wider mb-1.5">Skills</div>
|
|
376
|
+
{/* Selected skills tags */}
|
|
377
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
378
|
+
{editSkills.length === 0 && (
|
|
379
|
+
<span className="text-[11px] text-content-muted">No skills selected</span>
|
|
380
|
+
)}
|
|
381
|
+
{editSkills.map(skillId => (
|
|
382
|
+
<span
|
|
383
|
+
key={skillId}
|
|
384
|
+
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-accent/10 text-accent text-[11px] font-medium"
|
|
385
|
+
>
|
|
386
|
+
{skillId}
|
|
387
|
+
<button
|
|
388
|
+
onClick={() => removeSkill(skillId)}
|
|
389
|
+
className="hover:text-accent/70 transition-colors"
|
|
390
|
+
>
|
|
391
|
+
<X size={10} />
|
|
392
|
+
</button>
|
|
393
|
+
</span>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
{/* Add skills dropdown */}
|
|
397
|
+
<div className="relative">
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => setSkillPickerOpen(!skillPickerOpen)}
|
|
400
|
+
className="px-3 py-1.5 rounded-md border border-edge bg-surface text-[12px] text-content-muted hover:text-content hover:border-accent/30 transition-all"
|
|
401
|
+
>
|
|
402
|
+
{skillPickerOpen ? 'Close' : 'Add skills...'}
|
|
403
|
+
</button>
|
|
404
|
+
{skillPickerOpen && (
|
|
405
|
+
<div className="absolute z-10 mt-1 w-80 max-h-48 overflow-y-auto rounded-md border border-edge bg-panel shadow-lg">
|
|
406
|
+
{availableSkills.length === 0 ? (
|
|
407
|
+
<div className="p-3 text-[11px] text-content-muted">No skills available</div>
|
|
408
|
+
) : (
|
|
409
|
+
availableSkills.map(skill => {
|
|
410
|
+
const isSelected = editSkills.includes(skill.id);
|
|
411
|
+
return (
|
|
412
|
+
<button
|
|
413
|
+
key={skill.id}
|
|
414
|
+
onClick={() => toggleSkill(skill.id)}
|
|
415
|
+
className={`w-full text-left px-3 py-2 border-b border-edge/50 transition-all hover:bg-surface-raised ${
|
|
416
|
+
isSelected ? 'bg-accent/5' : ''
|
|
417
|
+
}`}
|
|
418
|
+
>
|
|
419
|
+
<div className="flex items-center gap-2">
|
|
420
|
+
<div
|
|
421
|
+
className={`w-3.5 h-3.5 rounded border flex items-center justify-center shrink-0 ${
|
|
422
|
+
isSelected
|
|
423
|
+
? 'bg-accent border-accent text-surface'
|
|
424
|
+
: 'border-edge'
|
|
425
|
+
}`}
|
|
426
|
+
>
|
|
427
|
+
{isSelected && (
|
|
428
|
+
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
|
|
429
|
+
<path d="M1.5 4L3 5.5L6.5 2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
430
|
+
</svg>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
<div className="min-w-0">
|
|
434
|
+
<div className="text-[12px] font-medium text-content">{skill.id}</div>
|
|
435
|
+
<div className="text-[10px] text-content-muted truncate">{skill.name}</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</button>
|
|
439
|
+
);
|
|
440
|
+
})
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{/* Grounding Template Editor */}
|
|
276
449
|
<div className="flex-1 flex overflow-hidden">
|
|
277
450
|
<div className="flex-1 flex flex-col p-4 overflow-hidden">
|
|
278
|
-
<div className="
|
|
451
|
+
<div className="flex items-center justify-between mb-2">
|
|
452
|
+
<div className="text-[11px] font-medium text-content-muted uppercase tracking-wider">Grounding Template</div>
|
|
453
|
+
{groundingModified && (
|
|
454
|
+
<Button variant="ghost" size="sm" icon={<RotateCcw size={10} />} onClick={handleReset}>
|
|
455
|
+
Reset Grounding to Default
|
|
456
|
+
</Button>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
279
459
|
<textarea
|
|
280
|
-
value={
|
|
281
|
-
onChange={e =>
|
|
460
|
+
value={editGrounding}
|
|
461
|
+
onChange={e => setEditGrounding(e.target.value)}
|
|
282
462
|
className="flex-1 w-full px-3 py-2 rounded-md border border-edge bg-surface text-[12px] text-content font-mono resize-none focus:border-accent/40 focus:outline-none"
|
|
283
|
-
placeholder="Enter agent
|
|
463
|
+
placeholder="Enter agent grounding template..."
|
|
284
464
|
spellCheck={false}
|
|
285
465
|
/>
|
|
286
466
|
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttachmentPreviewList — displays attached files as preview chips
|
|
3
|
+
* below the message input area before sending.
|
|
4
|
+
*
|
|
5
|
+
* Shows image thumbnails for image files, and a file icon with name
|
|
6
|
+
* for non-image files. Each chip has a remove button.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { X, FileText, FileCode, FileImage, File as FileIcon, Loader2 } from 'lucide-react';
|
|
10
|
+
import { AttachmentManager, type Attachment } from '../lib/attachment_manager';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
attachments: readonly Attachment[];
|
|
14
|
+
onRemove: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FILE_ICON_MAP: Record<string, typeof FileText> = {
|
|
18
|
+
'text/': FileText,
|
|
19
|
+
'application/json': FileCode,
|
|
20
|
+
'application/javascript': FileCode,
|
|
21
|
+
'application/typescript': FileCode,
|
|
22
|
+
'text/html': FileCode,
|
|
23
|
+
'text/css': FileCode,
|
|
24
|
+
'text/xml': FileCode,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getFileIcon = (mimeType: string) => {
|
|
28
|
+
if (AttachmentManager.isImage(mimeType)) return FileImage;
|
|
29
|
+
for (const [prefix, Icon] of Object.entries(FILE_ICON_MAP)) {
|
|
30
|
+
if (mimeType.startsWith(prefix)) return Icon;
|
|
31
|
+
}
|
|
32
|
+
return FileIcon;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const formatSize = (bytes: number): string => {
|
|
36
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
37
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
38
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function AttachmentPreviewList({ attachments, onRemove }: Props) {
|
|
42
|
+
if (attachments.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex flex-wrap gap-2 px-3 pb-2">
|
|
46
|
+
{attachments.map((att) => (
|
|
47
|
+
<AttachmentChip key={att.id} attachment={att} onRemove={onRemove} />
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ChipProps {
|
|
54
|
+
attachment: Attachment;
|
|
55
|
+
onRemove: (id: string) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AttachmentChip({ attachment, onRemove }: ChipProps) {
|
|
59
|
+
const isImage = AttachmentManager.isImage(attachment.mimeType);
|
|
60
|
+
const Icon = getFileIcon(attachment.mimeType);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="relative group flex items-center gap-2 bg-surface-alt border border-edge rounded-lg px-2.5 py-1.5 max-w-[200px]">
|
|
64
|
+
{/* Preview or icon */}
|
|
65
|
+
{isImage && attachment.preview ? (
|
|
66
|
+
<img
|
|
67
|
+
src={attachment.preview}
|
|
68
|
+
alt={attachment.name}
|
|
69
|
+
className="w-8 h-8 rounded object-cover flex-shrink-0"
|
|
70
|
+
/>
|
|
71
|
+
) : (
|
|
72
|
+
<Icon size={16} strokeWidth={1.5} className="text-content-muted flex-shrink-0" />
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Name and size */}
|
|
76
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
77
|
+
<span className="text-[11px] font-mono text-content truncate">{attachment.name}</span>
|
|
78
|
+
<span className="text-[10px] font-mono text-content-muted">
|
|
79
|
+
{attachment.uploading ? 'Uploading...' : formatSize(attachment.size)}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Upload spinner or remove button */}
|
|
84
|
+
{attachment.uploading ? (
|
|
85
|
+
<Loader2 size={12} className="text-accent animate-spin flex-shrink-0" />
|
|
86
|
+
) : (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => onRemove(attachment.id)}
|
|
90
|
+
className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-surface border border-edge flex items-center justify-center text-content-secondary cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity duration-150 hover:bg-error hover:text-white hover:border-error"
|
|
91
|
+
aria-label={`Remove ${attachment.name}`}
|
|
92
|
+
>
|
|
93
|
+
<X size={8} strokeWidth={2.5} />
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutocompleteDropdown — renders a scrollable list of autocomplete suggestions
|
|
3
|
+
* for file path (@) and skill (/) mentions in the chat input.
|
|
4
|
+
*
|
|
5
|
+
* Supports keyboard navigation (arrow keys highlight items) and mouse selection.
|
|
6
|
+
* Uses onMouseDown with preventDefault to avoid stealing focus from the textarea.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useEffect } from 'react';
|
|
10
|
+
import { FileText, Terminal } from 'lucide-react';
|
|
11
|
+
import type { AutocompleteItem } from '../hooks/use_mention_autocomplete';
|
|
12
|
+
|
|
13
|
+
interface AutocompleteDropdownProps {
|
|
14
|
+
items: AutocompleteItem[];
|
|
15
|
+
selectedIndex: number;
|
|
16
|
+
onSelect: (item: AutocompleteItem) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AutocompleteDropdown = ({ items, selectedIndex, onSelect }: AutocompleteDropdownProps) => {
|
|
20
|
+
const selectedRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
// Scroll selected item into view
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
selectedRef.current?.scrollIntoView({ block: 'nearest' });
|
|
25
|
+
}, [selectedIndex]);
|
|
26
|
+
|
|
27
|
+
if (items.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className="max-h-48 overflow-y-auto bg-surface border border-edge rounded-lg shadow-lg py-1"
|
|
32
|
+
role="listbox"
|
|
33
|
+
>
|
|
34
|
+
{items.map((item, index) => (
|
|
35
|
+
<div
|
|
36
|
+
key={item.value}
|
|
37
|
+
ref={index === selectedIndex ? selectedRef : undefined}
|
|
38
|
+
className={`flex items-center gap-2 px-3 py-1.5 cursor-pointer text-[13px] font-mono transition-colors duration-75 ${
|
|
39
|
+
index === selectedIndex
|
|
40
|
+
? 'bg-accent/10 text-accent'
|
|
41
|
+
: 'text-content hover:bg-surface-raised'
|
|
42
|
+
}`}
|
|
43
|
+
role="option"
|
|
44
|
+
aria-selected={index === selectedIndex}
|
|
45
|
+
onMouseDown={(e) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
onSelect(item);
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{item.type === 'file' ? (
|
|
51
|
+
<FileText size={12} className="shrink-0 text-content-muted" />
|
|
52
|
+
) : (
|
|
53
|
+
<Terminal size={12} className="shrink-0 text-content-muted" />
|
|
54
|
+
)}
|
|
55
|
+
<span className="truncate">{item.label}</span>
|
|
56
|
+
{item.description && (
|
|
57
|
+
<span className="ml-auto text-[11px] text-content-muted truncate max-w-[200px]">
|
|
58
|
+
{item.description}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatAttachButton — paperclip button to attach files via the native file picker.
|
|
3
|
+
*
|
|
4
|
+
* Opens the browser file dialog when clicked. Accepts any file type.
|
|
5
|
+
* Selected files are passed to the onFilesSelected callback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useRef, useCallback } from 'react';
|
|
9
|
+
import { Paperclip } from 'lucide-react';
|
|
10
|
+
import { IconButton } from './ds/IconButton';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
onFilesSelected: (files: File[]) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ChatAttachButton({ onFilesSelected, disabled }: Props) {
|
|
18
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
|
|
20
|
+
const handleClick = useCallback(() => {
|
|
21
|
+
inputRef.current?.click();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleChange = useCallback(() => {
|
|
25
|
+
const input = inputRef.current;
|
|
26
|
+
if (!input?.files || input.files.length === 0) return;
|
|
27
|
+
|
|
28
|
+
const files = Array.from(input.files);
|
|
29
|
+
onFilesSelected(files);
|
|
30
|
+
|
|
31
|
+
// Reset so the same file can be selected again
|
|
32
|
+
input.value = '';
|
|
33
|
+
}, [onFilesSelected]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<IconButton
|
|
38
|
+
label="Attach files"
|
|
39
|
+
variant="ghost"
|
|
40
|
+
size="sm"
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
onClick={handleClick}
|
|
43
|
+
>
|
|
44
|
+
<Paperclip size={14} strokeWidth={2} />
|
|
45
|
+
</IconButton>
|
|
46
|
+
<input
|
|
47
|
+
ref={inputRef}
|
|
48
|
+
type="file"
|
|
49
|
+
multiple
|
|
50
|
+
className="hidden"
|
|
51
|
+
onChange={handleChange}
|
|
52
|
+
tabIndex={-1}
|
|
53
|
+
/>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatDropZone — wraps the chat area to accept file drag-and-drop.
|
|
3
|
+
*
|
|
4
|
+
* When files are dragged over the zone, a visual overlay appears.
|
|
5
|
+
* On drop, the onFilesDropped callback is invoked with the dropped files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useRef, type DragEvent, type ReactNode } from 'react';
|
|
9
|
+
import { Upload } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
onFilesDropped: (files: File[]) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ChatDropZone({ children, onFilesDropped, disabled }: Props) {
|
|
18
|
+
const [dragOver, setDragOver] = useState(false);
|
|
19
|
+
const dragCounterRef = useRef(0);
|
|
20
|
+
|
|
21
|
+
const handleDragEnter = useCallback((e: DragEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
if (disabled) return;
|
|
25
|
+
dragCounterRef.current++;
|
|
26
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
27
|
+
setDragOver(true);
|
|
28
|
+
}
|
|
29
|
+
}, [disabled]);
|
|
30
|
+
|
|
31
|
+
const handleDragLeave = useCallback((e: DragEvent) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
dragCounterRef.current--;
|
|
35
|
+
if (dragCounterRef.current === 0) {
|
|
36
|
+
setDragOver(false);
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleDragOver = useCallback((e: DragEvent) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const handleDrop = useCallback((e: DragEvent) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
setDragOver(false);
|
|
49
|
+
dragCounterRef.current = 0;
|
|
50
|
+
|
|
51
|
+
if (disabled) return;
|
|
52
|
+
|
|
53
|
+
const files = Array.from(e.dataTransfer.files);
|
|
54
|
+
if (files.length > 0) {
|
|
55
|
+
onFilesDropped(files);
|
|
56
|
+
}
|
|
57
|
+
}, [disabled, onFilesDropped]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className="relative flex-1 flex flex-col min-h-0 min-w-0"
|
|
62
|
+
onDragEnter={handleDragEnter}
|
|
63
|
+
onDragLeave={handleDragLeave}
|
|
64
|
+
onDragOver={handleDragOver}
|
|
65
|
+
onDrop={handleDrop}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
|
|
69
|
+
{/* Drop overlay */}
|
|
70
|
+
{dragOver && (
|
|
71
|
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-accent/5 border-2 border-dashed border-accent/40 rounded-xl pointer-events-none">
|
|
72
|
+
<div className="flex flex-col items-center gap-2 text-accent">
|
|
73
|
+
<Upload size={32} strokeWidth={1.5} />
|
|
74
|
+
<span className="text-sm font-mono">Drop files to attach</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|