@burdenoff/microfe-vibecontrols 2026.830.1 → 2026.905.2

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.
Files changed (39) hide show
  1. package/dist/components/assistant/AIAssistantPanel.js +34 -67
  2. package/dist/components/assistant/AIAssistantPanel.js.map +1 -1
  3. package/dist/components/assistant/AssistantChatArea.js +11 -281
  4. package/dist/components/assistant/AssistantChatInput.js +4 -44
  5. package/dist/components/assistant/AssistantConversationList.js +6 -155
  6. package/dist/components/assistant/AssistantExportMenu.js +5 -52
  7. package/dist/components/assistant/AssistantMessageItem.js +9 -99
  8. package/dist/components/assistant/AssistantMessageList.js +4 -28
  9. package/dist/components/assistant/AssistantModeSelector.js +5 -60
  10. package/dist/components/assistant/AssistantWelcome.js +4 -42
  11. package/dist/components/assistant/index.js +1 -1
  12. package/dist/hooks/useAiCreditsQuota.js +8 -8
  13. package/dist/hooks/useWorkspaceWriteAccess.js +14 -0
  14. package/dist/hooks/useWorkspaceWriteAccess.js.map +1 -0
  15. package/dist/index.js +3 -3
  16. package/dist/pages/SessionsPage.js +6 -6
  17. package/dist/plugin.js +82 -64
  18. package/dist/plugin.js.map +1 -1
  19. package/dist/services/assistantApi.js +17 -48
  20. package/dist/services/assistantApi.js.map +1 -1
  21. package/dist/utils/assistantPrompt.js +34 -0
  22. package/dist/utils/assistantPrompt.js.map +1 -0
  23. package/dist/utils/pageContext.js +34 -15
  24. package/dist/utils/pageContext.js.map +1 -1
  25. package/package.json +2 -2
  26. package/dist/components/assistant/AssistantChatArea.js.map +0 -1
  27. package/dist/components/assistant/AssistantChatInput.js.map +0 -1
  28. package/dist/components/assistant/AssistantConversationList.js.map +0 -1
  29. package/dist/components/assistant/AssistantExportMenu.js.map +0 -1
  30. package/dist/components/assistant/AssistantMessageItem.js.map +0 -1
  31. package/dist/components/assistant/AssistantMessageList.js.map +0 -1
  32. package/dist/components/assistant/AssistantModeSelector.js.map +0 -1
  33. package/dist/components/assistant/AssistantWelcome.js.map +0 -1
  34. package/dist/hooks/useAssistantHeartbeat.js +0 -40
  35. package/dist/hooks/useAssistantHeartbeat.js.map +0 -1
  36. package/dist/hooks/useAssistantKeyboardShortcuts.js +0 -37
  37. package/dist/hooks/useAssistantKeyboardShortcuts.js.map +0 -1
  38. package/dist/services/assistantExportService.js +0 -44
  39. package/dist/services/assistantExportService.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantChatInput.js","names":[],"sources":["../../../src/components/assistant/AssistantChatInput.tsx"],"sourcesContent":["import { useRef, useCallback, type FC, type KeyboardEvent } from 'react';\nimport { Send } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n onSend: (message: string) => void;\n disabled?: boolean;\n placeholder?: string;\n}\n\nexport const AssistantChatInput: FC<Props> = ({\n onSend,\n disabled = false,\n placeholder = 'Ask anything...',\n}) => {\n const tr = useTr();\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSend = useCallback(() => {\n const textarea = textareaRef.current;\n if (!textarea) return;\n\n const value = textarea.value.trim();\n if (!value || disabled) return;\n\n onSend(value);\n textarea.value = '';\n textarea.style.height = 'auto';\n }, [onSend, disabled]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n },\n [handleSend]\n );\n\n const handleInput = useCallback(() => {\n const textarea = textareaRef.current;\n if (!textarea) return;\n textarea.style.height = 'auto';\n textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;\n }, []);\n\n return (\n <div className=\"flex items-end gap-2 px-3 py-2.5 border-t border-border-default bg-bg-surface\">\n <textarea\n ref={textareaRef}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n disabled={disabled}\n placeholder={placeholder}\n rows={1}\n className=\"flex-1 resize-none bg-bg-sunken border border-border-default rounded-lg px-3 py-2 text-xs text-text-primary placeholder:text-text-muted focus:outline-none focus:border-action-primary-bg disabled:opacity-50 leading-relaxed\"\n aria-label={tr('vibecontrols.assistant.chatInput.ariaLabel', 'Message input')}\n data-testid=\"assistant-input\"\n />\n <button\n type=\"button\"\n onClick={handleSend}\n disabled={disabled}\n className=\"flex-shrink-0 p-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n aria-label={tr('vibecontrols.assistant.chatInput.send', 'Send message')}\n data-testid=\"assistant-send\"\n >\n <Send className=\"size-3.5\" />\n </button>\n </div>\n );\n};\n"],"mappings":";;;;;AAUA,IAAa,KAAiC,EAC5C,WACA,cAAW,IACX,iBAAc,wBACV;CACJ,IAAM,IAAK,GAAO,EACZ,IAAc,EAA4B,KAAK,EAE/C,IAAa,QAAkB;EACnC,IAAM,IAAW,EAAY;AAC7B,MAAI,CAAC,EAAU;EAEf,IAAM,IAAQ,EAAS,MAAM,MAAM;AAC/B,GAAC,KAAS,MAEd,EAAO,EAAM,EACb,EAAS,QAAQ,IACjB,EAAS,MAAM,SAAS;IACvB,CAAC,GAAQ,EAAS,CAAC;AAmBtB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,YAAD;GACE,KAAK;GACL,WArBgB,GACnB,MAA0C;AACzC,IAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,aAC1B,EAAE,gBAAgB,EAClB,GAAY;MAGhB,CAAC,EAAW,CACb;GAcK,SAZc,QAAkB;IACpC,IAAM,IAAW,EAAY;AACxB,UACL,EAAS,MAAM,SAAS,QACxB,EAAS,MAAM,SAAS,GAAG,KAAK,IAAI,EAAS,cAAc,IAAI,CAAC;MAC/D,EAAE,CAAC;GAQU;GACG;GACb,MAAM;GACN,WAAU;GACV,cAAY,EAAG,8CAA8C,gBAAgB;GAC7E,eAAY;GACZ,CAAA,EACF,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACC;GACV,WAAU;GACV,cAAY,EAAG,yCAAyC,eAAe;GACvE,eAAY;aAEZ,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA;GACtB,CAAA,CACL"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantConversationList.js","names":[],"sources":["../../../src/components/assistant/AssistantConversationList.tsx"],"sourcesContent":["import { useState, type FC } from 'react';\nimport { Plus, Trash2, MessageSquare, X, Pencil, Check } from 'lucide-react';\nimport { useAssistantStore } from '@/store/assistantStore';\nimport { ASSISTANT_MODES } from '@/types/assistant';\nimport { useEffect } from 'react';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport const AssistantConversationList: FC = () => {\n const tr = useTr();\n const {\n conversations,\n activeConversationId,\n processingByConversationId,\n setActiveConversation,\n setMode,\n createConversation,\n deleteConversation,\n renameConversation,\n setConversationListOpen,\n } = useAssistantStore();\n\n const [editingId, setEditingId] = useState<string | null>(null);\n const [editTitle, setEditTitle] = useState('');\n\n useEffect(() => {\n if (activeConversationId) {\n setConversationListOpen(false);\n }\n }, [activeConversationId, setConversationListOpen]);\n\n const handleCreate = () => {\n createConversation();\n setConversationListOpen(false);\n };\n\n const handleSelect = (id: string) => {\n const selectedConversation = conversations.find((conversation) => conversation.id === id);\n setActiveConversation(id);\n if (selectedConversation) {\n setMode(selectedConversation.mode);\n }\n setConversationListOpen(false);\n };\n\n const handleStartRename = (id: string, currentTitle: string) => {\n setEditingId(id);\n setEditTitle(currentTitle);\n };\n\n const handleConfirmRename = () => {\n if (editingId && editTitle.trim()) {\n renameConversation(editingId, editTitle.trim());\n }\n setEditingId(null);\n };\n\n const handleDelete = (e: React.MouseEvent, id: string) => {\n e.stopPropagation();\n deleteConversation(id);\n };\n\n return (\n <div\n className=\"absolute inset-0 z-10 flex flex-col bg-bg-surface\"\n data-testid=\"conversation-list\"\n >\n {/* Header */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-border-default\">\n <span className=\"text-xs font-semibold text-text-primary\">\n {tr('vibecontrols.assistant.conversations.title', 'Conversations')}\n </span>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={handleCreate}\n className=\"p-1 hover:bg-bg-elevated rounded transition-colors\"\n aria-label={tr('vibecontrols.assistant.conversations.new', 'New conversation')}\n data-testid=\"new-conversation\"\n >\n <Plus className=\"size-3.5 text-text-secondary\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setConversationListOpen(false)}\n className=\"p-1 hover:bg-bg-elevated rounded transition-colors\"\n aria-label=\"Close conversation list\"\n >\n <X className=\"size-3.5 text-text-secondary\" />\n </button>\n </div>\n </div>\n\n {/* List */}\n <div className=\"flex-1 overflow-y-auto\">\n {conversations.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center h-full text-center px-4\">\n <MessageSquare className=\"size-6 text-text-muted mb-2\" />\n <p className=\"text-xs text-text-muted\">\n {tr('vibecontrols.assistant.conversations.empty', 'No conversations yet')}\n </p>\n <button\n type=\"button\"\n onClick={handleCreate}\n className=\"mt-2 text-xs text-action-primary-bg hover:underline\"\n >\n {tr('vibecontrols.assistant.conversations.startNew', 'Start a new conversation')}\n </button>\n </div>\n ) : (\n <div className=\"py-1\">\n {conversations.map((conv) => {\n const isActive = conv.id === activeConversationId;\n const isEditing = conv.id === editingId;\n const modeConfig = ASSISTANT_MODES.find((m) => m.id === conv.mode);\n const lastMessage = conv.messages[conv.messages.length - 1];\n const processing = processingByConversationId[conv.id];\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => !isEditing && handleSelect(conv.id))();\n }\n }}\n key={conv.id}\n onClick={() => !isEditing && handleSelect(conv.id)}\n className={`group px-3 py-2 cursor-pointer hover:bg-bg-elevated transition-colors ${\n isActive ? 'bg-bg-elevated border-l-2 border-action-primary-bg' : ''\n }`}\n >\n {isEditing ? (\n <div className=\"flex items-center gap-1\">\n <input\n aria-label=\"control\"\n value={editTitle}\n onChange={(e) => setEditTitle(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') handleConfirmRename();\n if (e.key === 'Escape') setEditingId(null);\n }}\n className=\"flex-1 text-xs bg-bg-sunken border border-border-default rounded px-2 py-1 text-text-primary focus:outline-none focus:border-action-primary-bg\"\n onClick={(e) => e.stopPropagation()}\n />\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleConfirmRename();\n }}\n className=\"p-1 hover:bg-bg-sunken rounded\"\n >\n <Check className=\"size-3 text-status-success-text\" />\n </button>\n </div>\n ) : (\n <>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs font-medium text-text-primary truncate flex-1\">\n {conv.title}\n </span>\n <div className=\"hidden group-hover:flex items-center gap-0.5 ml-1\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleStartRename(conv.id, conv.title);\n }}\n className=\"p-0.5 hover:bg-bg-sunken rounded\"\n aria-label=\"Rename conversation\"\n >\n <Pencil className=\"size-3 text-text-muted\" />\n </button>\n <button\n type=\"button\"\n onClick={(e) => handleDelete(e, conv.id)}\n className=\"p-0.5 hover:bg-bg-sunken rounded\"\n aria-label=\"Delete conversation\"\n data-testid=\"delete-conversation\"\n >\n <Trash2 className=\"size-3 text-status-error-text\" />\n </button>\n </div>\n </div>\n <div className=\"flex items-center gap-1.5 mt-0.5\">\n <span className=\"text-[10px] text-text-muted px-1 py-0.5 bg-bg-sunken rounded\">\n {modeConfig?.label ?? conv.mode}\n </span>\n <span className=\"text-[10px] text-text-muted\">\n {conv.messages.length} msgs\n </span>\n {processing && processing.status !== 'idle' && (\n <span className=\"text-[10px] text-action-primary-bg px-1 py-0.5 bg-bg-sunken rounded\">\n {processing.status}\n </span>\n )}\n </div>\n {lastMessage && (\n <p className=\"text-[10px] text-text-muted truncate mt-0.5\">\n {lastMessage.content.slice(0, 60)}\n </p>\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantExportMenu.js","names":[],"sources":["../../../src/components/assistant/AssistantExportMenu.tsx"],"sourcesContent":["import { useState, useRef, useEffect, type FC } from 'react';\nimport { Download, FileJson, FileText } from 'lucide-react';\nimport { useAssistantStore } from '@/store/assistantStore';\nimport { exportConversation, downloadFile } from '@/services/assistantExportService';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport const AssistantExportMenu: FC = () => {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const { conversations, activeConversationId } = useAssistantStore();\n const activeConversation = conversations.find((c) => c.id === activeConversationId);\n\n useEffect(() => {\n if (!open) return;\n const handleClickOutside = (e: MouseEvent) => {\n if (menuRef.current && !menuRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [open]);\n\n if (!activeConversation || activeConversation.messages.length === 0) return null;\n\n const handleExport = (format: 'json' | 'markdown') => {\n const { content, filename, mimeType } = exportConversation(activeConversation, format);\n downloadFile(content, filename, mimeType);\n setOpen(false);\n };\n\n return (\n <div ref={menuRef} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setOpen(!open)}\n className=\"p-1 hover:bg-bg-elevated rounded transition-colors\"\n aria-label={tr('vibecontrols.assistant.export.ariaLabel', 'Export conversation')}\n data-testid=\"assistant-export\"\n >\n <Download className=\"size-3.5 text-text-secondary\" />\n </button>\n\n {open && (\n <div className=\"absolute right-0 top-full mt-1 w-40 bg-bg-surface border border-border-default rounded-lg shadow-lg z-50 py-1\">\n <button\n type=\"button\"\n onClick={() => handleExport('json')}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-xs text-text-primary hover:bg-bg-elevated transition-colors\"\n data-testid=\"export-json\"\n >\n <FileJson className=\"size-3.5 text-text-secondary\" />\n {tr('vibecontrols.assistant.export.json', 'Export as JSON')}\n </button>\n <button\n type=\"button\"\n onClick={() => handleExport('markdown')}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-xs text-text-primary hover:bg-bg-elevated transition-colors\"\n data-testid=\"export-markdown\"\n >\n <FileText className=\"size-3.5 text-text-secondary\" />\n {tr('vibecontrols.assistant.export.markdown', 'Export as Markdown')}\n </button>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;AAMA,IAAa,UAAgC;CAC3C,IAAM,IAAK,GAAO,EACZ,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,IAAU,EAAuB,KAAK,EAEtC,EAAE,kBAAe,4BAAyB,GAAmB,EAC7D,IAAqB,EAAc,MAAM,MAAM,EAAE,OAAO,EAAqB;AAanF,KAXA,QAAgB;AACd,MAAI,CAAC,EAAM;EACX,IAAM,KAAsB,MAAkB;AAC5C,GAAI,EAAQ,WAAW,CAAC,EAAQ,QAAQ,SAAS,EAAE,OAAe,IAChE,EAAQ,GAAM;;AAIlB,SADA,SAAS,iBAAiB,aAAa,EAAmB,QAC7C,SAAS,oBAAoB,aAAa,EAAmB;IACzE,CAAC,EAAK,CAAC,EAEN,CAAC,KAAsB,EAAmB,SAAS,WAAW,EAAG,QAAO;CAE5E,IAAM,KAAgB,MAAgC;EACpD,IAAM,EAAE,YAAS,aAAU,gBAAa,EAAmB,GAAoB,EAAO;AAEtF,EADA,EAAa,GAAS,GAAU,EAAS,EACzC,EAAQ,GAAM;;AAGhB,QACE,kBAAC,OAAD;EAAK,KAAK;EAAS,WAAU;YAA7B,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAQ,CAAC,EAAK;GAC7B,WAAU;GACV,cAAY,EAAG,2CAA2C,sBAAsB;GAChF,eAAY;aAEZ,kBAAC,GAAD,EAAU,WAAU,gCAAiC,CAAA;GAC9C,CAAA,EAER,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAa,OAAO;IACnC,WAAU;IACV,eAAY;cAJd,CAME,kBAAC,GAAD,EAAU,WAAU,gCAAiC,CAAA,EACpD,EAAG,sCAAsC,iBAAiB,CACpD;OACT,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAa,WAAW;IACvC,WAAU;IACV,eAAY;cAJd,CAME,kBAAC,GAAD,EAAU,WAAU,gCAAiC,CAAA,EACpD,EAAG,0CAA0C,qBAAqB,CAC5D;MACL;KAEJ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantMessageItem.js","names":[],"sources":["../../../src/components/assistant/AssistantMessageItem.tsx"],"sourcesContent":["import { useMemo, useState, type FC } from 'react';\nimport { Bot, User } from 'lucide-react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { SaveToFilesButton } from '@burdenoff/fe-libs/storage';\nimport { AssistantMarkdownRenderer } from './AssistantMarkdownRenderer';\nimport { downloadAssistantArtifact } from '@/services/assistantApi';\nimport type { AssistantArtifact, AssistantMessageMetadata, Message } from '@/types/assistant';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n message: Message;\n isStreaming?: boolean;\n}\n\nfunction getAssistantMetadata(message: Message): AssistantMessageMetadata | undefined {\n return message.role === 'assistant' ? message.metadata : undefined;\n}\n\nfunction getArtifacts(metadata: AssistantMessageMetadata | undefined): AssistantArtifact[] {\n return metadata?.artifacts ?? [];\n}\n\nfunction decodeBase64(base64: string): string {\n return window.atob(base64);\n}\n\nfunction downloadBase64File(base64: string, filename: string, mimeType: string): void {\n const binary = decodeBase64(base64);\n const bytes = new Uint8Array(binary.length);\n\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n\n const blob = new Blob([bytes], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\n\nexport const AssistantMessageItem: FC<Props> = ({ message, isStreaming }) => {\n const tr = useTr();\n const apollo = useGatewayClient('workspace');\n const { getAccessToken, getWorkspaceToken } = useAuthToken();\n const [downloadingArtifactPath, setDownloadingArtifactPath] = useState<string | null>(null);\n const isUser = message.role === 'user';\n const metadata = useMemo(() => getAssistantMetadata(message), [message]);\n const artifacts = useMemo(() => getArtifacts(metadata), [metadata]);\n\n const handleArtifactDownload = async (artifact: AssistantArtifact) => {\n if (!metadata?.sandboxId || !metadata.sessionId) return;\n\n const workspaceId = new URLSearchParams(window.location.search).get('workspace') ?? '';\n if (!workspaceId) return;\n\n setDownloadingArtifactPath(artifact.path);\n\n try {\n const result = await downloadAssistantArtifact(\n apollo,\n metadata.sandboxId,\n workspaceId,\n metadata.sessionId,\n artifact.path,\n {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n }\n );\n\n downloadBase64File(result.contentBase64, result.filename, result.mimeType);\n } finally {\n setDownloadingArtifactPath(null);\n }\n };\n\n return (\n <div className={`flex gap-2.5 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>\n <div\n className={`flex-shrink-0 size-6 rounded-full flex items-center justify-center mt-0.5 ${\n isUser ? 'bg-action-primary-bg' : 'bg-bg-elevated border border-border-default'\n }`}\n >\n {isUser ? (\n <User className=\"size-3.5 text-action-primary-text\" />\n ) : (\n <Bot className=\"size-3.5 text-text-secondary\" />\n )}\n </div>\n\n <div\n className={`max-w-[85%] rounded-lg px-3 py-2 ${\n isUser\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-elevated border border-border-subtle'\n }`}\n >\n {isUser ? (\n <p className=\"text-xs whitespace-pre-wrap leading-relaxed\">{message.content}</p>\n ) : (\n <AssistantMarkdownRenderer content={message.content} />\n )}\n\n {artifacts.length > 0 && !isUser && metadata?.sandboxId && metadata.sessionId && (\n <div className=\"mt-3 space-y-2 border-t border-border-subtle pt-2\">\n <p className=\"text-[10px] font-medium uppercase tracking-wide text-text-muted\">\n {tr('vibecontrols.assistant.message.downloads', 'Downloads')}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {artifacts.map((artifact) => {\n const displayName = `${artifact.name}${artifact.type === 'directory' ? '.tar.gz' : ''}`;\n const isDownloading = downloadingArtifactPath === artifact.path;\n return (\n <SaveToFilesButton\n key={artifact.path}\n label={displayName}\n disabled={isDownloading}\n testId={`assistant-artifact-${artifact.name}`}\n source={{\n kind: 'sandbox-artifact',\n sandboxId: metadata.sandboxId!,\n sessionId: metadata.sessionId!,\n path: artifact.path,\n filename: displayName,\n }}\n defaultFolderPath=\"/Vibecontrols/AI Assistant/Artifacts\"\n onDownload={() => handleArtifactDownload(artifact)}\n />\n );\n })}\n </div>\n </div>\n )}\n\n {isStreaming && !isUser && (\n <span className=\"inline-block w-1.5 h-3.5 bg-text-secondary animate-pulse ml-0.5 align-middle rounded-sm\" />\n )}\n\n <div\n className={`text-[10px] mt-1 ${\n isUser ? 'text-action-primary-text/60' : 'text-text-muted'\n }`}\n >\n {new Date(message.timestamp).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n })}\n {message.tokens && !isUser && (\n <span className=\"ml-2\">{message.tokens.input + message.tokens.output} tokens</span>\n )}\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantMessageList.js","names":[],"sources":["../../../src/components/assistant/AssistantMessageList.tsx"],"sourcesContent":["import { useEffect, useRef, type FC } from 'react';\nimport { AssistantMessageItem } from './AssistantMessageItem';\nimport type { Message } from '@/types/assistant';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n messages: Message[];\n streamingMessageId: string | null;\n}\n\nexport const AssistantMessageList: FC<Props> = ({ messages, streamingMessageId }) => {\n const tr = useTr();\n const bottomRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Auto-scroll to bottom on new messages or streaming updates\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Only auto-scroll if user is near the bottom (within 100px)\n const isNearBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight < 100;\n\n if (isNearBottom) {\n bottomRef.current?.scrollIntoView({ behavior: 'smooth' });\n }\n }, [messages, streamingMessageId]);\n\n // Always scroll on first render\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: 'instant' });\n }, []);\n\n return (\n <div\n ref={containerRef}\n className=\"flex-1 overflow-y-auto p-3 space-y-3\"\n role=\"log\"\n aria-live=\"polite\"\n aria-label={tr('vibecontrols.assistant.messageList.ariaLabel', 'Conversation messages')}\n >\n {messages.map((message) => (\n <AssistantMessageItem\n key={message.id}\n message={message}\n isStreaming={message.id === streamingMessageId}\n />\n ))}\n <div ref={bottomRef} />\n </div>\n );\n};\n"],"mappings":";;;;;AAUA,IAAa,KAAmC,EAAE,aAAU,4BAAyB;CACnF,IAAM,IAAK,GAAO,EACZ,IAAY,EAAuB,KAAK,EACxC,IAAe,EAAuB,KAAK;AAqBjD,QAlBA,QAAgB;EACd,IAAM,IAAY,EAAa;AAC1B,OAIH,EAAU,eAAe,EAAU,YAAY,EAAU,eAAe,OAGxE,EAAU,SAAS,eAAe,EAAE,UAAU,UAAU,CAAC;IAE1D,CAAC,GAAU,EAAmB,CAAC,EAGlC,QAAgB;AACd,IAAU,SAAS,eAAe,EAAE,UAAU,WAAW,CAAC;IACzD,EAAE,CAAC,EAGJ,kBAAC,OAAD;EACE,KAAK;EACL,WAAU;EACV,MAAK;EACL,aAAU;EACV,cAAY,EAAG,gDAAgD,wBAAwB;YALzF,CAOG,EAAS,KAAK,MACb,kBAAC,GAAD;GAEW;GACT,aAAa,EAAQ,OAAO;GAC5B,EAHK,EAAQ,GAGb,CACF,EACF,kBAAC,OAAD,EAAK,KAAK,GAAa,CAAA,CACnB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantModeSelector.js","names":[],"sources":["../../../src/components/assistant/AssistantModeSelector.tsx"],"sourcesContent":["import { useState, useRef, useEffect, type FC } from 'react';\nimport { ChevronDown, Zap, Code, HelpCircle, Sparkles } from 'lucide-react';\nimport type { AssistantMode } from '@/types/assistant';\nimport { ASSISTANT_MODES } from '@/types/assistant';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n mode: AssistantMode;\n onModeChange: (mode: AssistantMode) => void;\n}\n\nconst MODE_ICONS: Record<AssistantMode, typeof Zap> = {\n assistant: Sparkles,\n 'api-calls': Code,\n 'vibe-plugins': Zap,\n general: HelpCircle,\n};\n\nexport const AssistantModeSelector: FC<Props> = ({ mode, onModeChange }) => {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const currentMode = ASSISTANT_MODES.find((m) => m.id === mode)!;\n const Icon = MODE_ICONS[mode];\n\n useEffect(() => {\n if (!open) return;\n const handleClickOutside = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setOpen(!open)}\n className=\"flex items-center gap-1 px-2 py-1 text-[10px] font-medium text-text-secondary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n aria-label={tr('vibecontrols.assistant.modeSelector.ariaLabel', 'Select assistant mode')}\n data-testid=\"assistant-mode-selector\"\n >\n <Icon className=\"size-3\" />\n <span>{currentMode.label}</span>\n <ChevronDown className={`size-3 transition-transform ${open ? 'rotate-180' : ''}`} />\n </button>\n\n {open && (\n <div className=\"absolute bottom-full left-0 mb-1 w-56 bg-bg-surface border border-border-default rounded-lg shadow-lg z-50 py-1\">\n {ASSISTANT_MODES.map((m) => {\n const ModeIcon = MODE_ICONS[m.id];\n const isActive = m.id === mode;\n return (\n <button\n type=\"button\"\n key={m.id}\n onClick={() => {\n onModeChange(m.id);\n setOpen(false);\n }}\n data-testid={`assistant-mode-option-${m.id}`}\n className={`w-full text-left px-3 py-2 flex items-start gap-2 hover:bg-bg-elevated transition-colors ${\n isActive ? 'bg-bg-elevated' : ''\n }`}\n >\n <ModeIcon className=\"size-3.5 mt-0.5 text-text-secondary flex-shrink-0\" />\n <div>\n <div className=\"text-xs font-medium text-text-primary\">{m.label}</div>\n <div className=\"text-[10px] text-text-muted leading-tight\">{m.description}</div>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAWA,IAAM,IAAgD;CACpD,WAAW;CACX,aAAa;CACb,gBAAgB;CAChB,SAAS;CACV,EAEY,KAAoC,EAAE,SAAM,sBAAmB;CAC1E,IAAM,IAAK,GAAO,EACZ,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,IAAe,EAAuB,KAAK,EAE3C,IAAc,EAAgB,MAAM,MAAM,EAAE,OAAO,EAAK,EACxD,IAAO,EAAW;AAaxB,QAXA,QAAgB;AACd,MAAI,CAAC,EAAM;EACX,IAAM,KAAsB,MAAkB;AAC5C,GAAI,EAAa,WAAW,CAAC,EAAa,QAAQ,SAAS,EAAE,OAAe,IAC1E,EAAQ,GAAM;;AAIlB,SADA,SAAS,iBAAiB,aAAa,EAAmB,QAC7C,SAAS,oBAAoB,aAAa,EAAmB;IACzE,CAAC,EAAK,CAAC,EAGR,kBAAC,OAAD;EAAK,KAAK;EAAc,WAAU;YAAlC,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAQ,CAAC,EAAK;GAC7B,WAAU;GACV,cAAY,EAAG,iDAAiD,wBAAwB;GACxF,eAAY;aALd;IAOE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;IAC3B,kBAAC,QAAD,EAAA,UAAO,EAAY,OAAa,CAAA;IAChC,kBAAC,GAAD,EAAa,WAAW,+BAA+B,IAAO,eAAe,MAAQ,CAAA;IAC9E;MAER,KACC,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAgB,KAAK,MAAM;IAC1B,IAAM,IAAW,EAAW,EAAE,KACxB,IAAW,EAAE,OAAO;AAC1B,WACE,kBAAC,UAAD;KACE,MAAK;KAEL,eAAe;AAEb,MADA,EAAa,EAAE,GAAG,EAClB,EAAQ,GAAM;;KAEhB,eAAa,yBAAyB,EAAE;KACxC,WAAW,4FACT,IAAW,mBAAmB;eATlC,CAYE,kBAAC,GAAD,EAAU,WAAU,qDAAsD,CAAA,EAC1E,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAyC,EAAE;MAAY,CAAA,EACtE,kBAAC,OAAD;MAAK,WAAU;gBAA6C,EAAE;MAAkB,CAAA,CAC5E,EAAA,CAAA,CACC;OAfF,EAAE,GAeA;KAEX;GACE,CAAA,CAEJ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssistantWelcome.js","names":[],"sources":["../../../src/components/assistant/AssistantWelcome.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { Bot, Sparkles } from 'lucide-react';\nimport type { AssistantMode } from '@/types/assistant';\nimport { ASSISTANT_MODES } from '@/types/assistant';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n mode: AssistantMode;\n onSuggestionClick: (suggestion: string) => void;\n}\n\nexport const AssistantWelcome: FC<Props> = ({ mode, onSuggestionClick }) => {\n const tr = useTr();\n const modeConfig = ASSISTANT_MODES.find((m) => m.id === mode)!;\n\n return (\n <div className=\"flex-1 flex flex-col items-center justify-center px-4 py-8\">\n <div className=\"size-10 rounded-full bg-bg-elevated border border-border-default flex items-center justify-center mb-3\">\n <Bot className=\"size-5 text-text-secondary\" />\n </div>\n <h3 className=\"text-sm font-semibold text-text-primary mb-1\">\n {tr('vibecontrols.assistant.welcome.title', 'VibeControls Assistant')}\n </h3>\n <p className=\"text-xs text-text-muted text-center mb-5 max-w-[240px]\">\n {modeConfig.description}\n </p>\n\n <div className=\"w-full space-y-2\">\n <div className=\"flex items-center gap-1 text-[10px] text-text-muted uppercase tracking-wider px-1\">\n <Sparkles className=\"size-3\" />\n <span>{tr('vibecontrols.assistant.welcome.suggestions', 'Suggestions')}</span>\n </div>\n {modeConfig.suggestions.map((suggestion) => (\n <button\n type=\"button\"\n key={suggestion}\n onClick={() => onSuggestionClick(suggestion)}\n className=\"w-full text-left px-3 py-2 text-xs text-text-primary bg-bg-sunken border border-border-subtle rounded-lg hover:bg-bg-elevated hover:border-border-default transition-colors leading-relaxed\"\n data-testid=\"assistant-suggestion\"\n >\n {suggestion}\n </button>\n ))}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;AAWA,IAAa,KAA+B,EAAE,SAAM,2BAAwB;CAC1E,IAAM,IAAK,GAAO,EACZ,IAAa,EAAgB,MAAM,MAAM,EAAE,OAAO,EAAK;AAE7D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAK,WAAU,8BAA+B,CAAA;IAC1C,CAAA;GACN,kBAAC,MAAD;IAAI,WAAU;cACX,EAAG,wCAAwC,yBAAyB;IAClE,CAAA;GACL,kBAAC,KAAD;IAAG,WAAU;cACV,EAAW;IACV,CAAA;GAEJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA,EAC/B,kBAAC,QAAD,EAAA,UAAO,EAAG,8CAA8C,cAAc,EAAQ,CAAA,CAC1E;QACL,EAAW,YAAY,KAAK,MAC3B,kBAAC,UAAD;KACE,MAAK;KAEL,eAAe,EAAkB,EAAW;KAC5C,WAAU;KACV,eAAY;eAEX;KACM,EANF,EAME,CACT,CACE;;GACF"}
@@ -1,40 +0,0 @@
1
- import { useAssistantStore as e } from "../store/assistantStore.js";
2
- import { extendAssistantSandboxTTL as t, sendHeartbeat as n } from "../services/assistantApi.js";
3
- import { useEffect as r } from "react";
4
- import { useGatewayClient as i } from "@burdenoff/fe-libs/shared/graphql";
5
- import { useAuthToken as a } from "@burdenoff/fe-libs/shared/providers/shell";
6
- //#region src/hooks/useAssistantHeartbeat.ts
7
- var o = 12e4;
8
- function s(s, c, l) {
9
- let u = i("workspace"), { getAccessToken: d, getWorkspaceToken: f } = a();
10
- r(() => {
11
- if (!s || !c || !l) return;
12
- let r = e.getState().sandboxesByMode[c]?.sandboxId;
13
- if (!r) return;
14
- let i = new URLSearchParams(window.location.search).get("workspace") ?? "";
15
- if (!i) return;
16
- let a = () => {
17
- let e = {
18
- accessToken: d(),
19
- workspaceToken: f()
20
- };
21
- Promise.all([n(u, r, i, s, e), t(u, r, i, 600, e)]).catch((e) => {
22
- console.warn("[heartbeat] Failed:", e);
23
- });
24
- };
25
- a();
26
- let p = setInterval(a, o);
27
- return () => clearInterval(p);
28
- }, [
29
- s,
30
- c,
31
- l,
32
- u,
33
- d,
34
- f
35
- ]);
36
- }
37
- //#endregion
38
- export { s as useAssistantHeartbeat };
39
-
40
- //# sourceMappingURL=useAssistantHeartbeat.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useAssistantHeartbeat.js","names":[],"sources":["../../src/hooks/useAssistantHeartbeat.ts"],"sourcesContent":["/**\n * Sends heartbeat pings to the ai-assistant every 2 minutes\n * while the assistant panel is open and a session is active.\n * Routes through: frontend → wspace-public-gateway → sandbox-svc → ai-assistant\n */\n\nimport { useEffect } from 'react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { extendAssistantSandboxTTL, sendHeartbeat } from '@/services/assistantApi';\nimport { useAssistantStore } from '@/store/assistantStore';\nimport type { AssistantMode } from '@/types/assistant';\n\nconst HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes\n\nexport function useAssistantHeartbeat(\n sessionId: string | null,\n mode: AssistantMode | null,\n isOpen: boolean\n): void {\n const apollo = useGatewayClient('workspace');\n const { getAccessToken, getWorkspaceToken } = useAuthToken();\n\n useEffect(() => {\n if (!sessionId || !mode || !isOpen) return;\n\n const sandboxId = useAssistantStore.getState().sandboxesByMode[mode]?.sandboxId;\n if (!sandboxId) return;\n\n const params = new URLSearchParams(window.location.search);\n const workspaceId = params.get('workspace') ?? '';\n if (!workspaceId) return;\n\n const ping = () => {\n const authContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n };\n\n Promise.all([\n sendHeartbeat(apollo, sandboxId, workspaceId, sessionId, authContext),\n extendAssistantSandboxTTL(apollo, sandboxId, workspaceId, 600, authContext),\n ]).catch((err) => {\n console.warn('[heartbeat] Failed:', err);\n });\n };\n\n ping();\n const interval = setInterval(ping, HEARTBEAT_INTERVAL_MS);\n\n return () => clearInterval(interval);\n }, [sessionId, mode, isOpen, apollo, getAccessToken, getWorkspaceToken]);\n}\n"],"mappings":";;;;;;AAaA,IAAM,IAAwB;AAE9B,SAAgB,EACd,GACA,GACA,GACM;CACN,IAAM,IAAS,EAAiB,YAAY,EACtC,EAAE,mBAAgB,yBAAsB,GAAc;AAE5D,SAAgB;AACd,MAAI,CAAC,KAAa,CAAC,KAAQ,CAAC,EAAQ;EAEpC,IAAM,IAAY,EAAkB,UAAU,CAAC,gBAAgB,IAAO;AACtE,MAAI,CAAC,EAAW;EAGhB,IAAM,IADS,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAC/B,IAAI,YAAY,IAAI;AAC/C,MAAI,CAAC,EAAa;EAElB,IAAM,UAAa;GACjB,IAAM,IAAc;IAClB,aAAa,GAAgB;IAC7B,gBAAgB,GAAmB;IACpC;AAED,WAAQ,IAAI,CACV,EAAc,GAAQ,GAAW,GAAa,GAAW,EAAY,EACrE,EAA0B,GAAQ,GAAW,GAAa,KAAK,EAAY,CAC5E,CAAC,CAAC,OAAO,MAAQ;AAChB,YAAQ,KAAK,uBAAuB,EAAI;KACxC;;AAGJ,KAAM;EACN,IAAM,IAAW,YAAY,GAAM,EAAsB;AAEzD,eAAa,cAAc,EAAS;IACnC;EAAC;EAAW;EAAM;EAAQ;EAAQ;EAAgB;EAAkB,CAAC"}
@@ -1,37 +0,0 @@
1
- import { useAssistantStore as e } from "../store/assistantStore.js";
2
- import { useEffect as t } from "react";
3
- //#region src/hooks/useAssistantKeyboardShortcuts.ts
4
- function n({ panelRef: n, inputRef: r, onClose: i }) {
5
- t(() => {
6
- let t = (t) => {
7
- let a = n.current;
8
- if (!a || !a.contains(document.activeElement) && document.activeElement !== a) return;
9
- let o = t.ctrlKey || t.metaKey;
10
- if (t.key === "Escape") {
11
- t.preventDefault(), i();
12
- return;
13
- }
14
- if (o && t.shiftKey && (t.key === "N" || t.key === "n")) {
15
- t.preventDefault(), e.getState().createConversation();
16
- return;
17
- }
18
- if (o && t.shiftKey && (t.key === "E" || t.key === "e")) {
19
- t.preventDefault(), a.querySelector("[data-testid=\"assistant-export\"]")?.click();
20
- return;
21
- }
22
- if (t.key === "/" && !o && !t.shiftKey) {
23
- let e = document.activeElement?.tagName;
24
- e !== "INPUT" && e !== "TEXTAREA" && (t.preventDefault(), r?.current?.focus());
25
- }
26
- };
27
- return document.addEventListener("keydown", t), () => document.removeEventListener("keydown", t);
28
- }, [
29
- n,
30
- r,
31
- i
32
- ]);
33
- }
34
- //#endregion
35
- export { n as useAssistantKeyboardShortcuts };
36
-
37
- //# sourceMappingURL=useAssistantKeyboardShortcuts.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useAssistantKeyboardShortcuts.js","names":[],"sources":["../../src/hooks/useAssistantKeyboardShortcuts.ts"],"sourcesContent":["import { useEffect, type RefObject } from 'react';\nimport { useAssistantStore } from '@/store/assistantStore';\n\ninterface Options {\n panelRef: RefObject<HTMLDivElement | null>;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n onClose: () => void;\n}\n\n/**\n * Panel-scoped keyboard shortcuts for the AI assistant.\n *\n * All shortcuts only fire when the assistant panel (or one of its children)\n * has focus, so they never override the host page's own shortcuts. We also\n * deliberately avoid the bare `Ctrl+N`/`Cmd+N`/`Ctrl+E`/`Ctrl+W` set the\n * browser owns at the OS level — those couldn't be cancelled reliably\n * anyway and were just adding visible \"shortcut listed, doesn't work\"\n * inconsistencies. The shortcuts here match the in-app shortcuts modal.\n *\n * - Escape: Close panel\n * - Ctrl/Cmd+Shift+N: New conversation\n * - Ctrl/Cmd+Shift+E: Export (triggers click on export button)\n * - /: Focus input (only when no input/textarea is already focused)\n */\nexport function useAssistantKeyboardShortcuts({ panelRef, inputRef, onClose }: Options) {\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n const panel = panelRef.current;\n if (!panel) return;\n\n // Only handle shortcuts when the panel or its children are focused.\n if (!panel.contains(document.activeElement) && document.activeElement !== panel) {\n return;\n }\n\n const isModKey = e.ctrlKey || e.metaKey;\n\n // Escape: close panel\n if (e.key === 'Escape') {\n e.preventDefault();\n onClose();\n return;\n }\n\n // Ctrl/Cmd+Shift+N: new conversation. Shift is required so we don't\n // fight the browser's `Ctrl+N` (new window).\n if (isModKey && e.shiftKey && (e.key === 'N' || e.key === 'n')) {\n e.preventDefault();\n useAssistantStore.getState().createConversation();\n return;\n }\n\n // Ctrl/Cmd+Shift+E: export. Shift required to avoid the browser's\n // `Ctrl+E` (search address bar in Chrome/Edge).\n if (isModKey && e.shiftKey && (e.key === 'E' || e.key === 'e')) {\n e.preventDefault();\n const exportBtn = panel.querySelector(\n '[data-testid=\"assistant-export\"]'\n ) as HTMLButtonElement | null;\n exportBtn?.click();\n return;\n }\n\n // /: focus input (when not in an input/textarea so single-key search\n // typing inside the page doesn't get hijacked).\n if (e.key === '/' && !isModKey && !e.shiftKey) {\n const tag = (document.activeElement as HTMLElement | null)?.tagName;\n if (tag !== 'INPUT' && tag !== 'TEXTAREA') {\n e.preventDefault();\n inputRef?.current?.focus();\n }\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [panelRef, inputRef, onClose]);\n}\n"],"mappings":";;;AAwBA,SAAgB,EAA8B,EAAE,aAAU,aAAU,cAAoB;AACtF,SAAgB;EACd,IAAM,KAAiB,MAAqB;GAC1C,IAAM,IAAQ,EAAS;AAIvB,OAHI,CAAC,KAGD,CAAC,EAAM,SAAS,SAAS,cAAc,IAAI,SAAS,kBAAkB,EACxE;GAGF,IAAM,IAAW,EAAE,WAAW,EAAE;AAGhC,OAAI,EAAE,QAAQ,UAAU;AAEtB,IADA,EAAE,gBAAgB,EAClB,GAAS;AACT;;AAKF,OAAI,KAAY,EAAE,aAAa,EAAE,QAAQ,OAAO,EAAE,QAAQ,MAAM;AAE9D,IADA,EAAE,gBAAgB,EAClB,EAAkB,UAAU,CAAC,oBAAoB;AACjD;;AAKF,OAAI,KAAY,EAAE,aAAa,EAAE,QAAQ,OAAO,EAAE,QAAQ,MAAM;AAE5C,IADlB,EAAE,gBAAgB,EACA,EAAM,cACtB,qCACD,EACU,OAAO;AAClB;;AAKF,OAAI,EAAE,QAAQ,OAAO,CAAC,KAAY,CAAC,EAAE,UAAU;IAC7C,IAAM,IAAO,SAAS,eAAsC;AAC5D,IAAI,MAAQ,WAAW,MAAQ,eAC7B,EAAE,gBAAgB,EAClB,GAAU,SAAS,OAAO;;;AAMhC,SADA,SAAS,iBAAiB,WAAW,EAAc,QACtC,SAAS,oBAAoB,WAAW,EAAc;IAClE;EAAC;EAAU;EAAU;EAAQ,CAAC"}
@@ -1,44 +0,0 @@
1
- //#region src/services/assistantExportService.ts
2
- function e(e, t) {
3
- let n = e.title.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(), r = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4
- if (t === "json") {
5
- let t = {
6
- id: e.id,
7
- title: e.title,
8
- mode: e.mode,
9
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
10
- backendSessionId: e.backendSessionId ?? null,
11
- messages: e.messages.filter((e) => e.role !== "system").map((e) => ({
12
- role: e.role,
13
- content: e.content,
14
- timestamp: e.timestamp,
15
- tokens: e.tokens
16
- }))
17
- };
18
- return {
19
- content: JSON.stringify(t, null, 2),
20
- filename: `${n}-${r}.json`,
21
- mimeType: "application/json"
22
- };
23
- }
24
- let i = [
25
- `# ${e.title}`,
26
- "",
27
- `> Mode: ${e.mode} | Exported: ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
28
- ""
29
- ];
30
- for (let t of e.messages) t.role !== "system" && (i.push(`## ${t.role === "user" ? "You" : "Assistant"}`), i.push(""), i.push(t.content), i.push(""));
31
- return {
32
- content: i.join("\n"),
33
- filename: `${n}-${r}.md`,
34
- mimeType: "text/markdown"
35
- };
36
- }
37
- function t(e, t, n) {
38
- let r = new Blob([e], { type: n }), i = URL.createObjectURL(r), a = document.createElement("a");
39
- a.href = i, a.download = t, document.body.appendChild(a), a.click(), document.body.removeChild(a), URL.revokeObjectURL(i);
40
- }
41
- //#endregion
42
- export { t as downloadFile, e as exportConversation };
43
-
44
- //# sourceMappingURL=assistantExportService.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assistantExportService.js","names":[],"sources":["../../src/services/assistantExportService.ts"],"sourcesContent":["import type { Conversation } from '@/types/assistant';\n\nexport function exportConversation(\n conversation: Conversation,\n format: 'json' | 'markdown'\n): { content: string; filename: string; mimeType: string } {\n const slug = conversation.title\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-|-$/g, '')\n .toLowerCase();\n const timestamp = new Date().toISOString().split('T')[0];\n\n if (format === 'json') {\n const data = {\n id: conversation.id,\n title: conversation.title,\n mode: conversation.mode,\n exportedAt: new Date().toISOString(),\n backendSessionId: conversation.backendSessionId ?? null,\n messages: conversation.messages\n .filter((message) => message.role !== 'system')\n .map((message) => ({\n role: message.role,\n content: message.content,\n timestamp: message.timestamp,\n tokens: message.tokens,\n })),\n };\n return {\n content: JSON.stringify(data, null, 2),\n filename: `${slug}-${timestamp}.json`,\n mimeType: 'application/json',\n };\n }\n\n const lines = [\n `# ${conversation.title}`,\n '',\n `> Mode: ${conversation.mode} | Exported: ${new Date().toLocaleString()}`,\n '',\n ];\n for (const message of conversation.messages) {\n if (message.role === 'system') continue;\n lines.push(`## ${message.role === 'user' ? 'You' : 'Assistant'}`);\n lines.push('');\n lines.push(message.content);\n lines.push('');\n }\n\n return {\n content: lines.join('\\n'),\n filename: `${slug}-${timestamp}.md`,\n mimeType: 'text/markdown',\n };\n}\n\nexport function downloadFile(content: string, filename: string, mimeType: string): void {\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\n"],"mappings":";AAEA,SAAgB,EACd,GACA,GACyD;CACzD,IAAM,IAAO,EAAa,MACvB,QAAQ,kBAAkB,IAAI,CAC9B,QAAQ,UAAU,GAAG,CACrB,aAAa,EACV,qBAAY,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC;AAEtD,KAAI,MAAW,QAAQ;EACrB,IAAM,IAAO;GACX,IAAI,EAAa;GACjB,OAAO,EAAa;GACpB,MAAM,EAAa;GACnB,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,kBAAkB,EAAa,oBAAoB;GACnD,UAAU,EAAa,SACpB,QAAQ,MAAY,EAAQ,SAAS,SAAS,CAC9C,KAAK,OAAa;IACjB,MAAM,EAAQ;IACd,SAAS,EAAQ;IACjB,WAAW,EAAQ;IACnB,QAAQ,EAAQ;IACjB,EAAE;GACN;AACD,SAAO;GACL,SAAS,KAAK,UAAU,GAAM,MAAM,EAAE;GACtC,UAAU,GAAG,EAAK,GAAG,EAAU;GAC/B,UAAU;GACX;;CAGH,IAAM,IAAQ;EACZ,KAAK,EAAa;EAClB;EACA,WAAW,EAAa,KAAK,gCAAe,IAAI,MAAM,EAAC,gBAAgB;EACvE;EACD;AACD,MAAK,IAAM,KAAW,EAAa,SAC7B,GAAQ,SAAS,aACrB,EAAM,KAAK,MAAM,EAAQ,SAAS,SAAS,QAAQ,cAAc,EACjE,EAAM,KAAK,GAAG,EACd,EAAM,KAAK,EAAQ,QAAQ,EAC3B,EAAM,KAAK,GAAG;AAGhB,QAAO;EACL,SAAS,EAAM,KAAK,KAAK;EACzB,UAAU,GAAG,EAAK,GAAG,EAAU;EAC/B,UAAU;EACX;;AAGH,SAAgB,EAAa,GAAiB,GAAkB,GAAwB;CACtF,IAAM,IAAO,IAAI,KAAK,CAAC,EAAQ,EAAE,EAAE,MAAM,GAAU,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAO,SAAS,cAAc,IAAI;AAMxC,CALA,EAAK,OAAO,GACZ,EAAK,WAAW,GAChB,SAAS,KAAK,YAAY,EAAK,EAC/B,EAAK,OAAO,EACZ,SAAS,KAAK,YAAY,EAAK,EAC/B,IAAI,gBAAgB,EAAI"}