@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
package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessageBubble — renders a single chat message as a user or assistant bubble.
|
|
3
|
+
*
|
|
4
|
+
* User bubbles: right-aligned, accent-tinted background, rounded corners.
|
|
5
|
+
* Includes inline image thumbnails and file chips for attachments.
|
|
6
|
+
* Assistant bubbles: left-aligned, surface background, full-width content area.
|
|
7
|
+
*
|
|
8
|
+
* Assistant messages delegate rendering to ChatMessageContent to handle
|
|
9
|
+
* text blocks, tool use cards, and tool result blocks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback } from 'react';
|
|
13
|
+
import type { ChatMessage, TextBlock, ImageBlock, FileBlock } from '../hooks/use_chat_stream';
|
|
14
|
+
import { ChatMessageContent } from './ChatMessageContent';
|
|
15
|
+
import { HighlightedText } from './HighlightedText';
|
|
16
|
+
import { ImageLightbox } from './ImageLightbox';
|
|
17
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
18
|
+
import { FileText, File as FileIcon } from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
interface ChatMessageBubbleProps {
|
|
21
|
+
message: ChatMessage;
|
|
22
|
+
searchQuery?: string;
|
|
23
|
+
activeMatchIndex?: number;
|
|
24
|
+
/** Global match-index offset for this message (so highlights can be globally addressed). */
|
|
25
|
+
matchIndexOffset?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Build the API URL for serving a chat attachment file.
|
|
29
|
+
* Paths are stored as `.chat-uploads/<filename>` — extract just the filename for the API. */
|
|
30
|
+
const buildFileUrl = (projectId: string, filePath: string): string => {
|
|
31
|
+
const fileName = filePath.replace(/^\.chat-uploads\//, '');
|
|
32
|
+
return `/api/chat-files/${encodeURIComponent(projectId)}/${encodeURIComponent(fileName)}`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Map common MIME types to a friendly label for file chips. */
|
|
36
|
+
const getFileTypeLabel = (mimeType: string): string => {
|
|
37
|
+
if (mimeType.startsWith('text/')) return 'TXT';
|
|
38
|
+
if (mimeType === 'application/pdf') return 'PDF';
|
|
39
|
+
if (mimeType === 'application/json') return 'JSON';
|
|
40
|
+
if (mimeType.includes('spreadsheet') || mimeType === 'text/csv') return 'CSV';
|
|
41
|
+
return 'FILE';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function ChatMessageBubble({ message, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageBubbleProps) {
|
|
45
|
+
if (message.role === 'user') {
|
|
46
|
+
return <UserBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
|
|
47
|
+
}
|
|
48
|
+
return <AssistantBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function UserBubble({ message, searchQuery, activeMatchIndex, matchIndexOffset }: { message: ChatMessage; searchQuery: string; activeMatchIndex: number; matchIndexOffset: number }) {
|
|
52
|
+
const textBlock = message.content.find((b) => b.type === 'text') as TextBlock | undefined;
|
|
53
|
+
const text = textBlock?.text ?? '';
|
|
54
|
+
const imageBlocks = message.content.filter((b): b is ImageBlock => b.type === 'image');
|
|
55
|
+
const fileBlocks = message.content.filter((b): b is FileBlock => b.type === 'file');
|
|
56
|
+
const projectId = useProjectStore((s) => s.selectedProjectId);
|
|
57
|
+
|
|
58
|
+
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
|
59
|
+
|
|
60
|
+
const handleImageClick = useCallback((index: number) => {
|
|
61
|
+
setLightboxIndex(index);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleLightboxClose = useCallback(() => {
|
|
65
|
+
setLightboxIndex(null);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex justify-end items-start gap-2">
|
|
70
|
+
<div className="max-w-[80%] overflow-hidden">
|
|
71
|
+
<div className="bg-accent/10 border border-accent/30 rounded-xl rounded-tr-sm px-4 py-2.5">
|
|
72
|
+
<pre className="m-0 text-[13px] font-mono text-content whitespace-pre-wrap break-words">
|
|
73
|
+
<HighlightedText text={text} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />
|
|
74
|
+
</pre>
|
|
75
|
+
|
|
76
|
+
{/* Image thumbnails */}
|
|
77
|
+
{imageBlocks.length > 0 && projectId && (
|
|
78
|
+
<div className="flex flex-wrap gap-2 mt-2">
|
|
79
|
+
{imageBlocks.map((img, idx) => (
|
|
80
|
+
<button
|
|
81
|
+
key={img.path}
|
|
82
|
+
type="button"
|
|
83
|
+
className="block w-20 h-20 rounded-md overflow-hidden border border-edge hover:border-accent transition-colors cursor-pointer bg-surface-alt shrink-0 p-0"
|
|
84
|
+
onClick={() => handleImageClick(idx)}
|
|
85
|
+
title={img.name}
|
|
86
|
+
>
|
|
87
|
+
<img
|
|
88
|
+
src={buildFileUrl(projectId, img.path)}
|
|
89
|
+
alt={img.name}
|
|
90
|
+
className="w-full h-full object-cover"
|
|
91
|
+
loading="lazy"
|
|
92
|
+
/>
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* File chips */}
|
|
99
|
+
{fileBlocks.length > 0 && (
|
|
100
|
+
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
101
|
+
{fileBlocks.map((file) => (
|
|
102
|
+
<span
|
|
103
|
+
key={file.path}
|
|
104
|
+
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-alt border border-edge text-[11px] font-mono text-content-secondary"
|
|
105
|
+
title={file.name}
|
|
106
|
+
>
|
|
107
|
+
{file.mimeType === 'application/pdf' ? (
|
|
108
|
+
<FileText size={12} className="text-content-muted shrink-0" />
|
|
109
|
+
) : (
|
|
110
|
+
<FileIcon size={12} className="text-content-muted shrink-0" />
|
|
111
|
+
)}
|
|
112
|
+
<span className="truncate max-w-32">{file.name}</span>
|
|
113
|
+
<span className="text-content-muted text-[10px]">{getFileTypeLabel(file.mimeType)}</span>
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Lightbox overlay */}
|
|
122
|
+
{lightboxIndex !== null && projectId && (
|
|
123
|
+
<ImageLightbox
|
|
124
|
+
images={imageBlocks.map((img) => ({
|
|
125
|
+
src: buildFileUrl(projectId, img.path),
|
|
126
|
+
alt: img.name,
|
|
127
|
+
}))}
|
|
128
|
+
initialIndex={lightboxIndex}
|
|
129
|
+
onClose={handleLightboxClose}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function AssistantBubble({ message, searchQuery, activeMatchIndex, matchIndexOffset }: { message: ChatMessage; searchQuery: string; activeMatchIndex: number; matchIndexOffset: number }) {
|
|
137
|
+
const isEmpty = message.content.length === 0;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex justify-start items-start gap-2">
|
|
141
|
+
<div className="max-w-[90%] min-w-0 overflow-hidden">
|
|
142
|
+
<div className="bg-surface-raised/50 border border-edge rounded-xl rounded-tl-sm px-4 py-2.5">
|
|
143
|
+
{isEmpty && message.isStreaming ? (
|
|
144
|
+
<span className="inline-flex gap-1 items-center text-[13px] font-mono text-content-muted">
|
|
145
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
|
146
|
+
Thinking…
|
|
147
|
+
</span>
|
|
148
|
+
) : (
|
|
149
|
+
<ChatMessageContent content={message.content} isStreaming={message.isStreaming} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessageContent — renders an assistant message's content blocks in order.
|
|
3
|
+
*
|
|
4
|
+
* Iterates over the ContentBlock array and renders:
|
|
5
|
+
* - TextBlock → markdown via react-markdown (safe, no dangerouslySetInnerHTML)
|
|
6
|
+
* - ToolUseBlock → collapsed ToolUseCard (shows tool input)
|
|
7
|
+
* - ToolResultBlock → collapsed ToolResultCard (shows tool output)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import ReactMarkdown from 'react-markdown';
|
|
11
|
+
import type { Components } from 'react-markdown';
|
|
12
|
+
import remarkGfm from 'remark-gfm';
|
|
13
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
14
|
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
15
|
+
import type { ContentBlock, ToolResultBlock, ToolUseBlock } from '../hooks/use_chat_stream';
|
|
16
|
+
import { ToolUseCard } from './ToolUseCard';
|
|
17
|
+
import { HighlightedText, countMatches } from './HighlightedText';
|
|
18
|
+
import { MermaidBlock } from './MermaidBlock';
|
|
19
|
+
|
|
20
|
+
interface ChatMessageContentProps {
|
|
21
|
+
content: ContentBlock[];
|
|
22
|
+
isStreaming: boolean;
|
|
23
|
+
searchQuery?: string;
|
|
24
|
+
activeMatchIndex?: number;
|
|
25
|
+
matchIndexOffset?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a lookup map from toolUseId → ToolResultBlock for quick access.
|
|
30
|
+
*/
|
|
31
|
+
const buildResultMap = (blocks: ContentBlock[]): Map<string, ToolResultBlock> => {
|
|
32
|
+
const map = new Map<string, ToolResultBlock>();
|
|
33
|
+
for (const block of blocks) {
|
|
34
|
+
if (block.type === 'tool_result') {
|
|
35
|
+
map.set(block.toolUseId, block);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/** Tailwind classes for the markdown container (replaces .chat-markdown CSS) */
|
|
43
|
+
const chatMarkdownClass = [
|
|
44
|
+
'text-[13px] font-mono text-content break-words min-w-0',
|
|
45
|
+
// first/last child spacing
|
|
46
|
+
'[&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
|
47
|
+
// headings
|
|
48
|
+
'[&_h1]:text-[16px] [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:leading-[1.3]',
|
|
49
|
+
'[&_h2]:text-[15px] [&_h2]:font-bold [&_h2]:mt-3.5 [&_h2]:mb-1.5 [&_h2]:leading-[1.3]',
|
|
50
|
+
'[&_h3]:text-[14px] [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:leading-[1.3]',
|
|
51
|
+
'[&_h4]:text-[13px] [&_h4]:font-semibold [&_h4]:mt-2.5 [&_h4]:mb-1 [&_h4]:leading-[1.3]',
|
|
52
|
+
// paragraphs
|
|
53
|
+
'[&_p]:my-1.5 [&_p]:leading-[1.6]',
|
|
54
|
+
// lists
|
|
55
|
+
'[&_ul]:list-disc [&_ol]:list-decimal [&_ul_ul]:list-circle [&_ol_ol]:list-[lower-alpha]',
|
|
56
|
+
'[&_ul]:pl-5 [&_ol]:pl-5 [&_ul]:my-1.5 [&_ol]:my-1.5',
|
|
57
|
+
'[&_li]:my-[3px] [&_li]:leading-normal [&_li>p]:my-0.5',
|
|
58
|
+
// blockquote
|
|
59
|
+
'[&_blockquote]:border-l-[3px] [&_blockquote]:border-edge [&_blockquote]:pl-3 [&_blockquote]:my-2 [&_blockquote]:text-content-secondary',
|
|
60
|
+
// pre / code
|
|
61
|
+
'[&_pre]:bg-surface-raised [&_pre]:py-2.5 [&_pre]:px-3 [&_pre]:rounded-md [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_pre]:my-2 [&_pre]:text-xs [&_pre]:leading-normal',
|
|
62
|
+
'[&_pre_code]:bg-transparent [&_pre_code]:p-0',
|
|
63
|
+
'[&_code]:bg-surface-raised [&_code]:px-[5px] [&_code]:py-px [&_code]:rounded-[3px] [&_code]:text-xs',
|
|
64
|
+
// inline
|
|
65
|
+
'[&_strong]:font-bold',
|
|
66
|
+
'[&_hr]:my-3 [&_hr]:border-edge',
|
|
67
|
+
// table
|
|
68
|
+
'[&_table]:my-2 [&_table]:border-collapse [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full',
|
|
69
|
+
'[&_th]:px-2.5 [&_th]:py-1 [&_th]:border [&_th]:border-edge [&_th]:text-xs [&_th]:font-semibold [&_th]:bg-surface-raised',
|
|
70
|
+
'[&_td]:px-2.5 [&_td]:py-1 [&_td]:border [&_td]:border-edge [&_td]:text-xs',
|
|
71
|
+
].join(' ');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Custom react-markdown components for syntax-highlighted code blocks.
|
|
75
|
+
* Fenced blocks (```lang) get Prism highlighting; inline `code` stays as a styled <code>.
|
|
76
|
+
*/
|
|
77
|
+
const markdownComponents: Components = {
|
|
78
|
+
code({ className, children, ...props }) {
|
|
79
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
80
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
81
|
+
|
|
82
|
+
// Fenced code block — has a language class set by react-markdown
|
|
83
|
+
if (match) {
|
|
84
|
+
// Mermaid code blocks render as interactive SVG diagrams
|
|
85
|
+
if (match[1] === 'mermaid') {
|
|
86
|
+
return <MermaidBlock code={codeString} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<SyntaxHighlighter
|
|
91
|
+
style={oneDark}
|
|
92
|
+
language={match[1]}
|
|
93
|
+
PreTag="div"
|
|
94
|
+
customStyle={{ margin: 0, borderRadius: 6, fontSize: 12 }}
|
|
95
|
+
>
|
|
96
|
+
{codeString}
|
|
97
|
+
</SyntaxHighlighter>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Inline code
|
|
102
|
+
return (
|
|
103
|
+
<code className={className} {...props}>
|
|
104
|
+
{children}
|
|
105
|
+
</code>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const ChatMessageContent = ({ content, isStreaming, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageContentProps) => {
|
|
111
|
+
const resultMap = buildResultMap(content);
|
|
112
|
+
|
|
113
|
+
// Pre-compute per-block match offsets so each text block knows its global offset
|
|
114
|
+
const blockOffsets: number[] = [];
|
|
115
|
+
let runningOffset = matchIndexOffset;
|
|
116
|
+
for (const block of content) {
|
|
117
|
+
blockOffsets.push(runningOffset);
|
|
118
|
+
if (block.type === 'text' && searchQuery) {
|
|
119
|
+
runningOffset += countMatches(block.text, searchQuery);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex flex-col gap-1 min-w-0 overflow-hidden">
|
|
125
|
+
{content.map((block, index) => {
|
|
126
|
+
if (block.type === 'text') {
|
|
127
|
+
// When there's an active search, render plain text with highlights
|
|
128
|
+
// instead of markdown to ensure match positions are accurate
|
|
129
|
+
if (searchQuery) {
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
key={`text-${index}`}
|
|
133
|
+
className={`${chatMarkdownClass} whitespace-pre-wrap`}
|
|
134
|
+
>
|
|
135
|
+
<HighlightedText
|
|
136
|
+
text={block.text}
|
|
137
|
+
searchQuery={searchQuery}
|
|
138
|
+
activeMatchIndex={activeMatchIndex}
|
|
139
|
+
matchIndexOffset={blockOffsets[index]}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
key={`text-${index}`}
|
|
148
|
+
className={chatMarkdownClass}
|
|
149
|
+
>
|
|
150
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
151
|
+
{block.text}
|
|
152
|
+
</ReactMarkdown>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (block.type === 'tool_use') {
|
|
158
|
+
const toolBlock = block as ToolUseBlock;
|
|
159
|
+
const matchedResult = toolBlock.id ? resultMap.get(toolBlock.id) : undefined;
|
|
160
|
+
const isLastBlock = index === content.length - 1;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<ToolUseCard
|
|
164
|
+
key={`tool-${toolBlock.id || index}`}
|
|
165
|
+
name={toolBlock.name}
|
|
166
|
+
input={toolBlock.input}
|
|
167
|
+
isStreaming={isStreaming && isLastBlock && !matchedResult}
|
|
168
|
+
result={matchedResult}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// tool_result blocks are rendered inline within their ToolUseCard — skip standalone rendering
|
|
174
|
+
if (block.type === 'tool_result') {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
})}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessageInput — multiline textarea with auto-expand, @ file and / skill
|
|
3
|
+
* mention autocomplete, and pill display for Chat v2.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Auto-expands vertically as user types, up to a max height
|
|
7
|
+
* - Enter sends, Shift+Enter and Option+Enter insert a newline
|
|
8
|
+
* - Integrated attach button (left) and send button (right)
|
|
9
|
+
* - @ triggers file path autocomplete, / at start triggers skill autocomplete
|
|
10
|
+
* - Resolved mentions are highlighted inline with accent styling
|
|
11
|
+
* - Disabled state when no session is selected or while uploading
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useRef, useCallback, useEffect, useMemo } from 'react';
|
|
15
|
+
import { SendHorizontal } from 'lucide-react';
|
|
16
|
+
import { IconButton } from './ds/IconButton';
|
|
17
|
+
import { ChatAttachButton } from './ChatAttachButton';
|
|
18
|
+
import { AutocompleteDropdown } from './AutocompleteDropdown';
|
|
19
|
+
import { useMentionAutocomplete } from '../hooks/use_mention_autocomplete';
|
|
20
|
+
import type { SkillInfo } from '../api/client';
|
|
21
|
+
|
|
22
|
+
interface ChatMessageInputProps {
|
|
23
|
+
value: string;
|
|
24
|
+
onChange: (value: string) => void;
|
|
25
|
+
onSend: () => void;
|
|
26
|
+
onFilesSelected: (files: File[]) => void;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
filePaths?: string[];
|
|
30
|
+
skills?: SkillInfo[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_HEIGHT = 200;
|
|
34
|
+
const MIN_HEIGHT = 42;
|
|
35
|
+
|
|
36
|
+
export function ChatMessageInput({
|
|
37
|
+
value,
|
|
38
|
+
onChange,
|
|
39
|
+
onSend,
|
|
40
|
+
onFilesSelected,
|
|
41
|
+
disabled,
|
|
42
|
+
placeholder = 'Type a message\u2026',
|
|
43
|
+
filePaths = [],
|
|
44
|
+
skills = [],
|
|
45
|
+
}: ChatMessageInputProps) {
|
|
46
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
showDropdown,
|
|
50
|
+
items,
|
|
51
|
+
selectedIndex,
|
|
52
|
+
mentions,
|
|
53
|
+
onTextChange,
|
|
54
|
+
onKeyDown: handleAutocompleteKeyDown,
|
|
55
|
+
selectItem,
|
|
56
|
+
} = useMentionAutocomplete({
|
|
57
|
+
filePaths,
|
|
58
|
+
skills,
|
|
59
|
+
value,
|
|
60
|
+
onChange,
|
|
61
|
+
textareaRef,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const adjustHeight = useCallback(() => {
|
|
65
|
+
const el = textareaRef.current;
|
|
66
|
+
if (!el) return;
|
|
67
|
+
el.style.height = 'auto';
|
|
68
|
+
el.style.height = `${Math.min(el.scrollHeight, MAX_HEIGHT)}px`;
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
adjustHeight();
|
|
73
|
+
}, [value, adjustHeight]);
|
|
74
|
+
|
|
75
|
+
const handleKeyDown = useCallback(
|
|
76
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
77
|
+
// Let autocomplete handle keys first
|
|
78
|
+
if (handleAutocompleteKeyDown(e)) return;
|
|
79
|
+
|
|
80
|
+
if (e.key === 'Enter') {
|
|
81
|
+
// Shift+Enter: default behavior (newline)
|
|
82
|
+
if (e.shiftKey) return;
|
|
83
|
+
|
|
84
|
+
// Option+Enter (Alt+Enter): insert newline manually
|
|
85
|
+
if (e.altKey) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const textarea = textareaRef.current;
|
|
88
|
+
if (textarea) {
|
|
89
|
+
const start = textarea.selectionStart;
|
|
90
|
+
const end = textarea.selectionEnd;
|
|
91
|
+
const newValue = value.slice(0, start) + '\n' + value.slice(end);
|
|
92
|
+
onChange(newValue);
|
|
93
|
+
requestAnimationFrame(() => {
|
|
94
|
+
textarea.selectionStart = start + 1;
|
|
95
|
+
textarea.selectionEnd = start + 1;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Plain Enter: send message
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
if (!disabled && value.trim()) {
|
|
104
|
+
onSend();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[disabled, value, onSend, onChange, handleAutocompleteKeyDown],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleChange = useCallback(
|
|
112
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
113
|
+
const newValue = e.target.value;
|
|
114
|
+
const cursorPos = e.target.selectionStart;
|
|
115
|
+
onChange(newValue);
|
|
116
|
+
onTextChange(newValue, cursorPos);
|
|
117
|
+
},
|
|
118
|
+
[onChange, onTextChange],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Detect trigger on cursor movement (click or arrow keys without value change)
|
|
122
|
+
const handleSelectionChange = useCallback(() => {
|
|
123
|
+
const textarea = textareaRef.current;
|
|
124
|
+
if (!textarea) return;
|
|
125
|
+
onTextChange(textarea.value, textarea.selectionStart);
|
|
126
|
+
}, [onTextChange]);
|
|
127
|
+
|
|
128
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
const hasMentions = mentions.length > 0;
|
|
130
|
+
|
|
131
|
+
// Sync overlay scroll with textarea
|
|
132
|
+
const handleScroll = useCallback(() => {
|
|
133
|
+
if (textareaRef.current && overlayRef.current) {
|
|
134
|
+
overlayRef.current.scrollTop = textareaRef.current.scrollTop;
|
|
135
|
+
}
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
// Build overlay content: plain text + highlighted mention spans
|
|
139
|
+
const overlayContent = useMemo(() => {
|
|
140
|
+
if (!hasMentions) return null;
|
|
141
|
+
|
|
142
|
+
const sorted = [...mentions].sort((a, b) => a.startIndex - b.startIndex);
|
|
143
|
+
const parts: React.ReactNode[] = [];
|
|
144
|
+
let lastEnd = 0;
|
|
145
|
+
|
|
146
|
+
sorted.forEach((mention, i) => {
|
|
147
|
+
if (mention.startIndex > lastEnd) {
|
|
148
|
+
parts.push(value.slice(lastEnd, mention.startIndex));
|
|
149
|
+
}
|
|
150
|
+
parts.push(
|
|
151
|
+
<span
|
|
152
|
+
key={`m${i}`}
|
|
153
|
+
className="bg-accent/15 text-accent rounded-[3px] ring-1 ring-accent/25"
|
|
154
|
+
>
|
|
155
|
+
{value.slice(mention.startIndex, mention.endIndex)}
|
|
156
|
+
</span>,
|
|
157
|
+
);
|
|
158
|
+
lastEnd = mention.endIndex;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (lastEnd < value.length) {
|
|
162
|
+
parts.push(value.slice(lastEnd));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return parts;
|
|
166
|
+
}, [value, mentions, hasMentions]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="relative">
|
|
170
|
+
{/* Autocomplete dropdown positioned above */}
|
|
171
|
+
{showDropdown && (
|
|
172
|
+
<div className="absolute bottom-full left-0 right-0 mb-1 z-50">
|
|
173
|
+
<AutocompleteDropdown
|
|
174
|
+
items={items}
|
|
175
|
+
selectedIndex={selectedIndex}
|
|
176
|
+
onSelect={selectItem}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
<div className="flex flex-col bg-surface-alt border border-edge rounded-xl focus-within:border-accent transition-colors duration-150">
|
|
182
|
+
{/* Input row */}
|
|
183
|
+
<div className="flex items-end gap-2 px-3 py-2">
|
|
184
|
+
<ChatAttachButton onFilesSelected={onFilesSelected} disabled={disabled} />
|
|
185
|
+
|
|
186
|
+
<div className="relative flex-1">
|
|
187
|
+
{/* Overlay behind textarea for inline mention highlights */}
|
|
188
|
+
{hasMentions && (
|
|
189
|
+
<div
|
|
190
|
+
ref={overlayRef}
|
|
191
|
+
className="absolute inset-0 text-[13px] font-mono text-content leading-relaxed whitespace-pre-wrap break-words overflow-hidden pointer-events-none"
|
|
192
|
+
aria-hidden="true"
|
|
193
|
+
>
|
|
194
|
+
{overlayContent}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
<textarea
|
|
199
|
+
ref={textareaRef}
|
|
200
|
+
value={value}
|
|
201
|
+
onChange={handleChange}
|
|
202
|
+
onKeyDown={handleKeyDown}
|
|
203
|
+
onSelect={handleSelectionChange}
|
|
204
|
+
onClick={handleSelectionChange}
|
|
205
|
+
onScroll={handleScroll}
|
|
206
|
+
disabled={disabled}
|
|
207
|
+
placeholder={placeholder}
|
|
208
|
+
rows={1}
|
|
209
|
+
className="relative w-full bg-transparent text-[13px] font-mono leading-relaxed placeholder:text-content-muted outline-none resize-none"
|
|
210
|
+
style={{
|
|
211
|
+
minHeight: `${MIN_HEIGHT - 16}px`,
|
|
212
|
+
maxHeight: `${MAX_HEIGHT}px`,
|
|
213
|
+
...(hasMentions
|
|
214
|
+
? { color: 'transparent', caretColor: 'var(--color-content)' }
|
|
215
|
+
: { color: 'var(--color-content)' }),
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<IconButton
|
|
221
|
+
label="Send message"
|
|
222
|
+
variant="accent"
|
|
223
|
+
size="sm"
|
|
224
|
+
disabled={disabled || !value.trim()}
|
|
225
|
+
onClick={onSend}
|
|
226
|
+
>
|
|
227
|
+
<SendHorizontal size={14} strokeWidth={2} />
|
|
228
|
+
</IconButton>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|