@mazees/mark 5.0.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/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { useRef, useEffect, useState, useMemo, useCallback, memo } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
FaMicrophone,
|
|
5
|
+
FaStop,
|
|
6
|
+
FaArrowUp,
|
|
7
|
+
FaSmile,
|
|
8
|
+
FaPaperclip,
|
|
9
|
+
FaTimes,
|
|
10
|
+
FaFileAlt,
|
|
11
|
+
FaFilePdf,
|
|
12
|
+
FaFileCode,
|
|
13
|
+
FaFileImage,
|
|
14
|
+
FaLock,
|
|
15
|
+
FaFolder
|
|
16
|
+
} from 'react-icons/fa'
|
|
17
|
+
import { Zap, Folder, Wrench, ChevronLeft } from 'lucide-react'
|
|
18
|
+
import ConfirmModal from './ConfirmModal'
|
|
19
|
+
import { NATIVE_SKILLS } from './native-skills'
|
|
20
|
+
import { calculateSessionTokens, MAX_CONTEXT_TOKENS } from '../../api/ai/contextManager'
|
|
21
|
+
import {
|
|
22
|
+
getChatData,
|
|
23
|
+
getSessionCompact,
|
|
24
|
+
getSessionAutoMode,
|
|
25
|
+
setSessionAutoMode
|
|
26
|
+
} from '../../api/db'
|
|
27
|
+
|
|
28
|
+
const EMOJIS = [
|
|
29
|
+
'😂',
|
|
30
|
+
'🤣',
|
|
31
|
+
'😅',
|
|
32
|
+
'🗿',
|
|
33
|
+
'🙏',
|
|
34
|
+
'🔥',
|
|
35
|
+
'🚀',
|
|
36
|
+
'💀',
|
|
37
|
+
'😎',
|
|
38
|
+
'🤔',
|
|
39
|
+
'😭',
|
|
40
|
+
'❤️',
|
|
41
|
+
'👍',
|
|
42
|
+
'✨',
|
|
43
|
+
'👀',
|
|
44
|
+
'💯'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const formatFileSize = (bytes) => {
|
|
48
|
+
if (!bytes) return ''
|
|
49
|
+
if (bytes < 1024) return bytes + ' B'
|
|
50
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
51
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getFileIcon = (fileName = '') => {
|
|
55
|
+
const ext = fileName.split('.').pop().toLowerCase()
|
|
56
|
+
if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext))
|
|
57
|
+
return <FaFileImage className="text-primary" />
|
|
58
|
+
if (['pdf'].includes(ext)) return <FaFilePdf className="text-error" />
|
|
59
|
+
if (['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'json', 'py', 'cpp', 'cs'].includes(ext))
|
|
60
|
+
return <FaFileCode className="text-info" />
|
|
61
|
+
return <FaFileAlt className="text-primary" />
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const InputBar = ({
|
|
65
|
+
sessionId = '1',
|
|
66
|
+
onSubmit,
|
|
67
|
+
isLoading,
|
|
68
|
+
isRecording,
|
|
69
|
+
isProcessing,
|
|
70
|
+
audioIntensity = 0,
|
|
71
|
+
onStartRecord,
|
|
72
|
+
onStopRecord,
|
|
73
|
+
onStop,
|
|
74
|
+
source = 'pc',
|
|
75
|
+
inline = false,
|
|
76
|
+
className = '',
|
|
77
|
+
workspaceRoot = null,
|
|
78
|
+
onSelectWorkspace = null,
|
|
79
|
+
onManualCompact = null
|
|
80
|
+
}) => {
|
|
81
|
+
void source
|
|
82
|
+
const inputRef = useRef(null)
|
|
83
|
+
const fileInputRef = useRef(null)
|
|
84
|
+
const contextPopoverRef = useRef(null)
|
|
85
|
+
const gaugeRef = useRef(null)
|
|
86
|
+
const mentionListRef = useRef(null)
|
|
87
|
+
const skillListRef = useRef(null)
|
|
88
|
+
const [inputText, setInputText] = useState('')
|
|
89
|
+
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
|
90
|
+
const [showAbortConfirm, setShowAbortConfirm] = useState(false)
|
|
91
|
+
const [showContextPopover, setShowContextPopover] = useState(false)
|
|
92
|
+
const [attachedFiles, setAttachedFiles] = useState([])
|
|
93
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
94
|
+
const lastPromptRef = useRef('')
|
|
95
|
+
const isPastingRef = useRef(false)
|
|
96
|
+
|
|
97
|
+
const [skills, setSkills] = useState([])
|
|
98
|
+
const [filteredSkills, setFilteredSkills] = useState([])
|
|
99
|
+
const [showSkillList, setShowSkillList] = useState(false)
|
|
100
|
+
const [selectedSkillIndex, setSelectedSkillIndex] = useState(0)
|
|
101
|
+
|
|
102
|
+
// Mention (@) Autocomplete States
|
|
103
|
+
const [showMentionList, setShowMentionList] = useState(false)
|
|
104
|
+
const [mentionCategory, setMentionCategory] = useState('all') // 'all' | 'files' | 'tools'
|
|
105
|
+
const [mentionQuery, setMentionQuery] = useState('')
|
|
106
|
+
const [mentionCursorIndex, setMentionCursorIndex] = useState(0)
|
|
107
|
+
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
|
|
108
|
+
const [workspaceFiles, setWorkspaceFiles] = useState([])
|
|
109
|
+
const [isLoadingFiles, setIsLoadingFiles] = useState(false)
|
|
110
|
+
const [toolGroups, setToolGroups] = useState([])
|
|
111
|
+
|
|
112
|
+
const loadToolGroups = useCallback(async () => {
|
|
113
|
+
if (window.api && window.api.getGroupTools) {
|
|
114
|
+
try {
|
|
115
|
+
const data = await window.api.getGroupTools()
|
|
116
|
+
const schema = data?.schema || {}
|
|
117
|
+
const names = data?.names || Object.keys(schema)
|
|
118
|
+
const list = names.map((key) => {
|
|
119
|
+
const desc = schema[key]?.description || ''
|
|
120
|
+
const label = key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
121
|
+
return {
|
|
122
|
+
name: key,
|
|
123
|
+
label,
|
|
124
|
+
description: desc
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
setToolGroups(list)
|
|
128
|
+
return list
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[InputBar] Failed to load tool groups:', err)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return []
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
loadToolGroups()
|
|
138
|
+
}, [loadToolGroups])
|
|
139
|
+
|
|
140
|
+
const [contextTracker, setContextTracker] = useState({
|
|
141
|
+
currentTokens: 0,
|
|
142
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
143
|
+
percentage: 0,
|
|
144
|
+
lastCompactedAt: null
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const [isAutoMode, setIsAutoMode] = useState(false)
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
let isMounted = true
|
|
151
|
+
getSessionAutoMode(sessionId).then((val) => {
|
|
152
|
+
if (isMounted) setIsAutoMode(Boolean(val))
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const handleAutoModeUpdate = (e) => {
|
|
156
|
+
if (String(e.detail?.sessionId) === String(sessionId)) {
|
|
157
|
+
setIsAutoMode(Boolean(e.detail?.isAutoMode))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
window.addEventListener('session-auto-mode-updated', handleAutoModeUpdate)
|
|
162
|
+
return () => {
|
|
163
|
+
isMounted = false
|
|
164
|
+
window.removeEventListener('session-auto-mode-updated', handleAutoModeUpdate)
|
|
165
|
+
}
|
|
166
|
+
}, [sessionId])
|
|
167
|
+
|
|
168
|
+
const handleToggleAutoMode = async () => {
|
|
169
|
+
const next = !isAutoMode
|
|
170
|
+
setIsAutoMode(next)
|
|
171
|
+
await setSessionAutoMode(sessionId, next)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const handleClickOutside = (e) => {
|
|
176
|
+
if (
|
|
177
|
+
contextPopoverRef.current &&
|
|
178
|
+
!contextPopoverRef.current.contains(e.target) &&
|
|
179
|
+
(!gaugeRef.current || !gaugeRef.current.contains(e.target))
|
|
180
|
+
) {
|
|
181
|
+
setShowContextPopover(false)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (showContextPopover) {
|
|
185
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
186
|
+
}
|
|
187
|
+
return () => {
|
|
188
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
189
|
+
}
|
|
190
|
+
}, [showContextPopover])
|
|
191
|
+
|
|
192
|
+
// Muat status konteks awal sesi saat mount atau saat sessionId berganti
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let isCancelled = false
|
|
195
|
+
const loadInitialContext = async () => {
|
|
196
|
+
try {
|
|
197
|
+
const [messages, compact] = await Promise.all([
|
|
198
|
+
getChatData(sessionId),
|
|
199
|
+
getSessionCompact(String(sessionId))
|
|
200
|
+
])
|
|
201
|
+
if (isCancelled) return
|
|
202
|
+
const tokens = calculateSessionTokens(
|
|
203
|
+
messages || [],
|
|
204
|
+
compact?.summaryBlock || compact?.summary_block || '',
|
|
205
|
+
compact?.lastCompactedMessageId || compact?.last_compacted_message_id || null,
|
|
206
|
+
'',
|
|
207
|
+
compact?.lastCompactedAt || compact?.last_compacted_at || null
|
|
208
|
+
)
|
|
209
|
+
setContextTracker({
|
|
210
|
+
currentTokens: tokens,
|
|
211
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
212
|
+
percentage: Math.min(100, (tokens / MAX_CONTEXT_TOKENS) * 100),
|
|
213
|
+
lastCompactedAt: compact?.lastCompactedAt || null
|
|
214
|
+
})
|
|
215
|
+
} catch {
|
|
216
|
+
// ignore
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
loadInitialContext()
|
|
220
|
+
return () => {
|
|
221
|
+
isCancelled = true
|
|
222
|
+
}
|
|
223
|
+
}, [sessionId])
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const handleTrackerUpdate = (e) => {
|
|
227
|
+
if (e.detail) {
|
|
228
|
+
if (e.detail.sessionId && String(e.detail.sessionId) !== String(sessionId)) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
const tokens = Number(e.detail.currentTokens ?? e.detail.currentChars ?? 0)
|
|
232
|
+
const maxTokens = Number(e.detail.maxTokens ?? e.detail.maxChars ?? MAX_CONTEXT_TOKENS)
|
|
233
|
+
setContextTracker({
|
|
234
|
+
currentTokens: tokens,
|
|
235
|
+
maxTokens,
|
|
236
|
+
percentage: Number(
|
|
237
|
+
e.detail.percentage !== undefined
|
|
238
|
+
? e.detail.percentage
|
|
239
|
+
: maxTokens > 0
|
|
240
|
+
? Math.min(100, (tokens / maxTokens) * 100)
|
|
241
|
+
: 0
|
|
242
|
+
),
|
|
243
|
+
lastCompactedAt: e.detail.lastCompactedAt || null
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
window.addEventListener('context-tracker-updated', handleTrackerUpdate)
|
|
249
|
+
return () => {
|
|
250
|
+
window.removeEventListener('context-tracker-updated', handleTrackerUpdate)
|
|
251
|
+
}
|
|
252
|
+
}, [sessionId])
|
|
253
|
+
|
|
254
|
+
const reloadSkills = async () => {
|
|
255
|
+
if (window.api && window.api.getSkills) {
|
|
256
|
+
try {
|
|
257
|
+
const loadedSkills = await window.api.getSkills()
|
|
258
|
+
const nativeSkillList = NATIVE_SKILLS.map((s) => ({
|
|
259
|
+
name: s.name,
|
|
260
|
+
description: s.description
|
|
261
|
+
}))
|
|
262
|
+
const merged = [...nativeSkillList, ...(loadedSkills || [])]
|
|
263
|
+
setSkills(merged)
|
|
264
|
+
return merged
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error('[InputBar] Failed to reload skills:', err)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
reloadSkills()
|
|
274
|
+
if (window.api && window.api.onSkillsUpdated) {
|
|
275
|
+
const unsub = window.api.onSkillsUpdated(() => {
|
|
276
|
+
reloadSkills()
|
|
277
|
+
})
|
|
278
|
+
return () => {
|
|
279
|
+
if (typeof unsub === 'function') unsub()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}, [])
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!isLoading && inputRef.current) {
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
if (inputRef.current) inputRef.current.focus()
|
|
288
|
+
}, 50)
|
|
289
|
+
}
|
|
290
|
+
}, [isLoading])
|
|
291
|
+
|
|
292
|
+
// Auto-scroll item terpilih ke viewport saat navigasi keyboard
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (showMentionList && mentionListRef.current) {
|
|
295
|
+
const activeEl = mentionListRef.current.children[selectedMentionIndex]
|
|
296
|
+
if (activeEl && typeof activeEl.scrollIntoView === 'function') {
|
|
297
|
+
activeEl.scrollIntoView({ block: 'nearest' })
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}, [selectedMentionIndex, showMentionList])
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (showSkillList && skillListRef.current) {
|
|
304
|
+
const activeEl = skillListRef.current.children[selectedSkillIndex]
|
|
305
|
+
if (activeEl && typeof activeEl.scrollIntoView === 'function') {
|
|
306
|
+
activeEl.scrollIntoView({ block: 'nearest' })
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}, [selectedSkillIndex, showSkillList])
|
|
310
|
+
|
|
311
|
+
const handleFileChange = (e) => {
|
|
312
|
+
const files = Array.from(e.target.files || [])
|
|
313
|
+
addFiles(files)
|
|
314
|
+
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const handlePaperclipClick = async () => {
|
|
318
|
+
if (window.api && window.api.showOpenDialog) {
|
|
319
|
+
try {
|
|
320
|
+
const filePaths = await window.api.showOpenDialog()
|
|
321
|
+
if (filePaths && filePaths.length > 0) {
|
|
322
|
+
const dialogFiles = filePaths.map((p) => ({
|
|
323
|
+
name: p.split(/[/\\]/).pop(),
|
|
324
|
+
path: p,
|
|
325
|
+
size: 0,
|
|
326
|
+
type: ''
|
|
327
|
+
}))
|
|
328
|
+
setAttachedFiles((prev) => {
|
|
329
|
+
const existingPaths = new Set(prev.map((item) => item.path))
|
|
330
|
+
const unique = dialogFiles.filter((item) => !existingPaths.has(item.path))
|
|
331
|
+
return [...prev, ...unique]
|
|
332
|
+
})
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error('[InputBar] Open dialog error:', err)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
fileInputRef.current?.click()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const handlePaste = async (e) => {
|
|
343
|
+
if (isPastingRef.current) return
|
|
344
|
+
const clipboardData = e.clipboardData || window.clipboardData
|
|
345
|
+
if (!clipboardData) return
|
|
346
|
+
|
|
347
|
+
let imageFiles = []
|
|
348
|
+
|
|
349
|
+
// 1. Ambil file bawaan dari clipboardData.files (sudah ter-deduplikasi oleh browser)
|
|
350
|
+
if (clipboardData.files && clipboardData.files.length > 0) {
|
|
351
|
+
imageFiles = Array.from(clipboardData.files).filter(
|
|
352
|
+
(f) => f.type && f.type.startsWith('image/')
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 2. Fallback jika files kosong, cek clipboardData.items (hanya ambil file unik pertama)
|
|
357
|
+
if (imageFiles.length === 0 && clipboardData.items && clipboardData.items.length > 0) {
|
|
358
|
+
for (let i = 0; i < clipboardData.items.length; i++) {
|
|
359
|
+
const item = clipboardData.items[i]
|
|
360
|
+
if (item.type && item.type.startsWith('image/')) {
|
|
361
|
+
const file = item.getAsFile()
|
|
362
|
+
if (file) {
|
|
363
|
+
const ext = item.type.split('/')[1] || 'png'
|
|
364
|
+
const namedFile = new File([file], `screenshot-${Date.now()}.${ext}`, {
|
|
365
|
+
type: item.type
|
|
366
|
+
})
|
|
367
|
+
imageFiles.push(namedFile)
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (imageFiles.length > 0) {
|
|
375
|
+
e.preventDefault()
|
|
376
|
+
e.stopPropagation()
|
|
377
|
+
isPastingRef.current = true
|
|
378
|
+
try {
|
|
379
|
+
await addFiles(imageFiles)
|
|
380
|
+
} finally {
|
|
381
|
+
setTimeout(() => {
|
|
382
|
+
isPastingRef.current = false
|
|
383
|
+
}, 300)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const addFiles = async (newFiles) => {
|
|
389
|
+
const parsedFiles = await Promise.all(
|
|
390
|
+
newFiles.map(async (f) => {
|
|
391
|
+
let resolvedPath = ''
|
|
392
|
+
let previewUrl = null
|
|
393
|
+
|
|
394
|
+
// Baca Base64 untuk preview jika file gambar
|
|
395
|
+
if (f.type && f.type.startsWith('image/')) {
|
|
396
|
+
try {
|
|
397
|
+
previewUrl = await new Promise((res) => {
|
|
398
|
+
const reader = new FileReader()
|
|
399
|
+
reader.onload = () => res(reader.result)
|
|
400
|
+
reader.onerror = () => res(null)
|
|
401
|
+
reader.readAsDataURL(f)
|
|
402
|
+
})
|
|
403
|
+
} catch {
|
|
404
|
+
// ignore preview error
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (window.api && window.api.getPathForFile) {
|
|
409
|
+
try {
|
|
410
|
+
resolvedPath = window.api.getPathForFile(f)
|
|
411
|
+
} catch (e) {
|
|
412
|
+
console.error('[InputBar] getPathForFile error:', e)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const isRealDiskPath =
|
|
417
|
+
resolvedPath &&
|
|
418
|
+
resolvedPath !== f.name &&
|
|
419
|
+
(resolvedPath.includes('/') || resolvedPath.includes('\\'))
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
!isRealDiskPath &&
|
|
423
|
+
f.path &&
|
|
424
|
+
f.path !== f.name &&
|
|
425
|
+
(f.path.includes('/') || f.path.includes('\\'))
|
|
426
|
+
) {
|
|
427
|
+
resolvedPath = f.path
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Jika file belum memiliki path disk asli (misal pasted image atau drag-drop browser)
|
|
431
|
+
if (
|
|
432
|
+
!resolvedPath ||
|
|
433
|
+
resolvedPath === f.name ||
|
|
434
|
+
(!resolvedPath.includes('/') && !resolvedPath.includes('\\'))
|
|
435
|
+
) {
|
|
436
|
+
try {
|
|
437
|
+
if (window.api?.saveTempFile && previewUrl) {
|
|
438
|
+
const tempPath = await window.api.saveTempFile(previewUrl, f.name)
|
|
439
|
+
if (tempPath) resolvedPath = tempPath
|
|
440
|
+
} else {
|
|
441
|
+
const buffer = await f.arrayBuffer()
|
|
442
|
+
if (buffer && buffer.byteLength > 0 && window.api?.saveTempFile) {
|
|
443
|
+
const tempPath = await window.api.saveTempFile(buffer, f.name)
|
|
444
|
+
if (tempPath) resolvedPath = tempPath
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error('[InputBar] Failed to save dragged/pasted file to temp:', err)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!resolvedPath && previewUrl) {
|
|
453
|
+
resolvedPath = previewUrl
|
|
454
|
+
}
|
|
455
|
+
if (!resolvedPath) resolvedPath = f.name
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
name: f.name,
|
|
459
|
+
path: resolvedPath,
|
|
460
|
+
size: f.size,
|
|
461
|
+
type: f.type,
|
|
462
|
+
previewUrl: previewUrl || (resolvedPath.startsWith('data:image/') ? resolvedPath : null)
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
setAttachedFiles((prev) => {
|
|
468
|
+
const existingPaths = new Set(prev.map((p) => p.path))
|
|
469
|
+
const unique = parsedFiles.filter((p) => !existingPaths.has(p.path))
|
|
470
|
+
return [...prev, ...unique]
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
setTimeout(() => {
|
|
474
|
+
if (inputRef.current) inputRef.current.focus()
|
|
475
|
+
}, 50)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const removeFile = (indexToRemove) => {
|
|
479
|
+
setAttachedFiles((prev) => prev.filter((_, idx) => idx !== indexToRemove))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const handleDragOver = (e) => {
|
|
483
|
+
e.preventDefault()
|
|
484
|
+
e.stopPropagation()
|
|
485
|
+
if (!isDragging) setIsDragging(true)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const handleDragLeave = (e) => {
|
|
489
|
+
e.preventDefault()
|
|
490
|
+
e.stopPropagation()
|
|
491
|
+
setIsDragging(false)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const handleDrop = (e) => {
|
|
495
|
+
e.preventDefault()
|
|
496
|
+
e.stopPropagation()
|
|
497
|
+
setIsDragging(false)
|
|
498
|
+
|
|
499
|
+
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
500
|
+
const files = Array.from(e.dataTransfer.files)
|
|
501
|
+
addFiles(files)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const handleFormSubmit = async () => {
|
|
506
|
+
let finalPrompt = inputText
|
|
507
|
+
let userText = inputText
|
|
508
|
+
const skillMatches = inputText.match(/(?:\s|^)\/([a-zA-Z0-9_-]+)/g)
|
|
509
|
+
|
|
510
|
+
if (skillMatches && skillMatches.length > 0 && window.api && window.api.readSkill) {
|
|
511
|
+
let combinedSkillsContent = ''
|
|
512
|
+
const loadedSkills = []
|
|
513
|
+
|
|
514
|
+
for (const match of skillMatches) {
|
|
515
|
+
const skillName = match.trim().substring(1) // Hilangkan spasi dan '/'
|
|
516
|
+
|
|
517
|
+
// INTERCEPT BUILT-IN SKILLS
|
|
518
|
+
const nativeSkill = NATIVE_SKILLS.find(
|
|
519
|
+
(s) => s.name.toLowerCase() === skillName.toLowerCase()
|
|
520
|
+
)
|
|
521
|
+
if (nativeSkill) {
|
|
522
|
+
combinedSkillsContent += `\n\n--- SKILL BAWAAN: ${skillName.toUpperCase()} ---\n${nativeSkill.content}`
|
|
523
|
+
loadedSkills.push(skillName)
|
|
524
|
+
userText = userText.replace(match, '')
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const skillData = await window.api.readSkill(skillName)
|
|
530
|
+
if (skillData) {
|
|
531
|
+
// Support both old string format and new object format
|
|
532
|
+
const content = typeof skillData === 'string' ? skillData : skillData.content
|
|
533
|
+
const basePath =
|
|
534
|
+
typeof skillData === 'object' && skillData.basePath ? skillData.basePath : ''
|
|
535
|
+
|
|
536
|
+
combinedSkillsContent += `\n\n--- SKILL EXTERNAL: ${skillName.toUpperCase()} ---\n`
|
|
537
|
+
if (basePath) {
|
|
538
|
+
combinedSkillsContent += `[LOKASI ABSOLUT SKILL INI (Base Path): ${basePath}]\n\n`
|
|
539
|
+
}
|
|
540
|
+
combinedSkillsContent += `${content}`
|
|
541
|
+
|
|
542
|
+
loadedSkills.push(skillName)
|
|
543
|
+
userText = userText.replace(match, '') // Hapus slash command dari teks yang dilihat AI
|
|
544
|
+
}
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.error('[InputBar] Failed to read skill:', skillName, e)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
userText = userText.trim()
|
|
551
|
+
|
|
552
|
+
if (loadedSkills.length > 0) {
|
|
553
|
+
finalPrompt = `${userText}\n\n=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===\nBerikut adalah instruksi skill khusus yang WAJIB kamu kombinasikan dan ikuti secara ketat untuk mengeksekusi permintaan di atas. Jika skill memiliki referensi sub-file, kamu BISA membacanya menggunakan tool "read-file" dengan menggabungkan "LOKASI ABSOLUT" di bawah ini beserta path relatifnya:\n${combinedSkillsContent}\n=========================================`
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const currentAttachments = [...attachedFiles]
|
|
558
|
+
if (attachedFiles.length > 0) {
|
|
559
|
+
const filePathsText = attachedFiles.map((f) => `"${f.path}"`).join(', ')
|
|
560
|
+
if (finalPrompt.trim()) {
|
|
561
|
+
finalPrompt = `${finalPrompt.trim()}\n\n[FILE TERLAMPIR]: ${filePathsText}`
|
|
562
|
+
} else {
|
|
563
|
+
finalPrompt = `Tolong proses/rangkum file terlampir ini.\n\n[FILE TERLAMPIR]: ${filePathsText}`
|
|
564
|
+
}
|
|
565
|
+
setAttachedFiles([])
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (finalPrompt.trim()) {
|
|
569
|
+
if (!isLoading) {
|
|
570
|
+
lastPromptRef.current = inputText
|
|
571
|
+
}
|
|
572
|
+
const rawUserText = inputText.trim()
|
|
573
|
+
setInputText('')
|
|
574
|
+
if (typeof onSubmit === 'function') {
|
|
575
|
+
onSubmit(finalPrompt, {
|
|
576
|
+
displayPrompt: rawUserText,
|
|
577
|
+
attachedFiles: currentAttachments
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const handleEmojiClick = (emoji) => {
|
|
584
|
+
setInputText((prev) => prev + emoji)
|
|
585
|
+
setShowEmojiPicker(false)
|
|
586
|
+
setTimeout(() => {
|
|
587
|
+
if (inputRef.current) inputRef.current.focus()
|
|
588
|
+
}, 50)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const fetchWorkspaceFiles = async (query = '') => {
|
|
592
|
+
if (window.api && window.api.getWorkspaceFiles) {
|
|
593
|
+
setIsLoadingFiles(true)
|
|
594
|
+
try {
|
|
595
|
+
const files = await window.api.getWorkspaceFiles(query, workspaceRoot)
|
|
596
|
+
setWorkspaceFiles(files || [])
|
|
597
|
+
} catch (err) {
|
|
598
|
+
console.error('[InputBar] getWorkspaceFiles error:', err)
|
|
599
|
+
} finally {
|
|
600
|
+
setIsLoadingFiles(false)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const mentionDisplayItems = useMemo(() => {
|
|
606
|
+
if (mentionCategory === 'all' && !mentionQuery.trim()) {
|
|
607
|
+
return [
|
|
608
|
+
{
|
|
609
|
+
type: 'category',
|
|
610
|
+
category: 'files',
|
|
611
|
+
title: 'Files',
|
|
612
|
+
subtitle: 'Cari berkas di workspace'
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
type: 'category',
|
|
616
|
+
category: 'tools',
|
|
617
|
+
title: 'Tool Groups',
|
|
618
|
+
subtitle: 'Kelompok kapabilitas tool agen'
|
|
619
|
+
}
|
|
620
|
+
]
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const q = mentionQuery.toLowerCase().trim()
|
|
624
|
+
const items = []
|
|
625
|
+
|
|
626
|
+
// 1. Tool Groups
|
|
627
|
+
if (mentionCategory === 'all' || mentionCategory === 'tools') {
|
|
628
|
+
const matchedTools = toolGroups
|
|
629
|
+
.filter(
|
|
630
|
+
(t) =>
|
|
631
|
+
!q ||
|
|
632
|
+
t.name.toLowerCase().includes(q) ||
|
|
633
|
+
t.label.toLowerCase().includes(q) ||
|
|
634
|
+
t.description.toLowerCase().includes(q)
|
|
635
|
+
)
|
|
636
|
+
.map((t) => ({
|
|
637
|
+
type: 'tool',
|
|
638
|
+
name: t.name,
|
|
639
|
+
label: t.label,
|
|
640
|
+
description: t.description
|
|
641
|
+
}))
|
|
642
|
+
items.push(...matchedTools)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 2. Files
|
|
646
|
+
if (mentionCategory === 'all' || mentionCategory === 'files') {
|
|
647
|
+
const matchedFiles = workspaceFiles
|
|
648
|
+
.filter(
|
|
649
|
+
(f) => !q || f.name.toLowerCase().includes(q) || f.relativePath.toLowerCase().includes(q)
|
|
650
|
+
)
|
|
651
|
+
.map((f) => ({
|
|
652
|
+
type: 'file',
|
|
653
|
+
name: f.name,
|
|
654
|
+
relativePath: f.relativePath,
|
|
655
|
+
absolutePath: f.absolutePath,
|
|
656
|
+
size: f.size,
|
|
657
|
+
ext: f.ext
|
|
658
|
+
}))
|
|
659
|
+
items.push(...matchedFiles)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return items
|
|
663
|
+
}, [mentionCategory, mentionQuery, toolGroups, workspaceFiles])
|
|
664
|
+
|
|
665
|
+
const selectMentionItem = (item) => {
|
|
666
|
+
if (!item) return
|
|
667
|
+
|
|
668
|
+
if (item.type === 'category') {
|
|
669
|
+
setMentionCategory(item.category)
|
|
670
|
+
setSelectedMentionIndex(0)
|
|
671
|
+
if (item.category === 'files') {
|
|
672
|
+
fetchWorkspaceFiles('')
|
|
673
|
+
} else if (item.category === 'tools' && toolGroups.length === 0) {
|
|
674
|
+
loadToolGroups()
|
|
675
|
+
}
|
|
676
|
+
if (inputRef.current) inputRef.current.focus()
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const val = inputText
|
|
681
|
+
const beforeAt = val.slice(0, mentionCursorIndex)
|
|
682
|
+
const afterMention = val.slice(mentionCursorIndex + 1 + mentionQuery.length)
|
|
683
|
+
|
|
684
|
+
if (item.type === 'file') {
|
|
685
|
+
const insertTag = `@${item.relativePath} `
|
|
686
|
+
const nextText = `${beforeAt}${insertTag}${afterMention}`
|
|
687
|
+
setInputText(nextText)
|
|
688
|
+
|
|
689
|
+
// Lampirkan otomatis ke attachedFiles
|
|
690
|
+
const newAttached = {
|
|
691
|
+
name: item.name,
|
|
692
|
+
path: item.absolutePath,
|
|
693
|
+
size: item.size,
|
|
694
|
+
type: item.ext ? `text/${item.ext.replace('.', '')}` : '',
|
|
695
|
+
previewUrl: null
|
|
696
|
+
}
|
|
697
|
+
setAttachedFiles((prev) => {
|
|
698
|
+
const exists = prev.some((f) => f.path === newAttached.path)
|
|
699
|
+
return exists ? prev : [...prev, newAttached]
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
setShowMentionList(false)
|
|
703
|
+
setMentionCategory('all')
|
|
704
|
+
setMentionQuery('')
|
|
705
|
+
setTimeout(() => {
|
|
706
|
+
if (inputRef.current) {
|
|
707
|
+
const newPos = beforeAt.length + insertTag.length
|
|
708
|
+
inputRef.current.focus()
|
|
709
|
+
inputRef.current.setSelectionRange(newPos, newPos)
|
|
710
|
+
}
|
|
711
|
+
}, 50)
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (item.type === 'tool') {
|
|
716
|
+
const insertTag = `@${item.name} `
|
|
717
|
+
const nextText = `${beforeAt}${insertTag}${afterMention}`
|
|
718
|
+
setInputText(nextText)
|
|
719
|
+
|
|
720
|
+
setShowMentionList(false)
|
|
721
|
+
setMentionCategory('all')
|
|
722
|
+
setMentionQuery('')
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
if (inputRef.current) {
|
|
725
|
+
const newPos = beforeAt.length + insertTag.length
|
|
726
|
+
inputRef.current.focus()
|
|
727
|
+
inputRef.current.setSelectionRange(newPos, newPos)
|
|
728
|
+
}
|
|
729
|
+
}, 50)
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const handleTextChange = async (e) => {
|
|
735
|
+
const val = e.target.value
|
|
736
|
+
setInputText(val)
|
|
737
|
+
|
|
738
|
+
const cursor = e.target.selectionStart ?? val.length
|
|
739
|
+
const textBefore = val.slice(0, cursor)
|
|
740
|
+
|
|
741
|
+
// 1. Deteksi Slash Command (/):
|
|
742
|
+
if (val.startsWith('/')) {
|
|
743
|
+
const currentSkills = skills && skills.length > 0 ? skills : await reloadSkills()
|
|
744
|
+
const query = val.slice(1).toLowerCase()
|
|
745
|
+
const matches = currentSkills.filter((s) => s.name.toLowerCase().includes(query))
|
|
746
|
+
setFilteredSkills(matches)
|
|
747
|
+
setShowSkillList(true)
|
|
748
|
+
setSelectedSkillIndex(0)
|
|
749
|
+
setShowMentionList(false)
|
|
750
|
+
return
|
|
751
|
+
} else {
|
|
752
|
+
setShowSkillList(false)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 2. Deteksi Mention Tag (@):
|
|
756
|
+
const atIndex = textBefore.lastIndexOf('@')
|
|
757
|
+
if (atIndex !== -1 && (atIndex === 0 || /\s/.test(textBefore[atIndex - 1]))) {
|
|
758
|
+
const afterAt = textBefore.slice(atIndex + 1)
|
|
759
|
+
if (!/\s/.test(afterAt)) {
|
|
760
|
+
if (toolGroups.length === 0) {
|
|
761
|
+
loadToolGroups()
|
|
762
|
+
}
|
|
763
|
+
setShowMentionList(true)
|
|
764
|
+
setMentionQuery(afterAt)
|
|
765
|
+
setMentionCursorIndex(atIndex)
|
|
766
|
+
setSelectedMentionIndex(0)
|
|
767
|
+
fetchWorkspaceFiles(afterAt)
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
setShowMentionList(false)
|
|
773
|
+
setMentionCategory('all')
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const selectSkill = (skillObj) => {
|
|
777
|
+
setInputText(`/${skillObj.name} `)
|
|
778
|
+
setShowSkillList(false)
|
|
779
|
+
if (inputRef.current) inputRef.current.focus()
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const handleKeyDown = (e) => {
|
|
783
|
+
// 1. Skill List Navigation
|
|
784
|
+
if (showSkillList && filteredSkills.length > 0) {
|
|
785
|
+
if (e.key === 'ArrowDown') {
|
|
786
|
+
e.preventDefault()
|
|
787
|
+
setSelectedSkillIndex((prev) => (prev + 1) % filteredSkills.length)
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
if (e.key === 'ArrowUp') {
|
|
791
|
+
e.preventDefault()
|
|
792
|
+
setSelectedSkillIndex((prev) => (prev - 1 + filteredSkills.length) % filteredSkills.length)
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
796
|
+
e.preventDefault()
|
|
797
|
+
selectSkill(filteredSkills[selectedSkillIndex])
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
if (e.key === 'Escape') {
|
|
801
|
+
setShowSkillList(false)
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 2. Mention (@) Navigation
|
|
807
|
+
if (showMentionList && mentionDisplayItems.length > 0) {
|
|
808
|
+
if (e.key === 'ArrowDown') {
|
|
809
|
+
e.preventDefault()
|
|
810
|
+
setSelectedMentionIndex((prev) => (prev + 1) % mentionDisplayItems.length)
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
if (e.key === 'ArrowUp') {
|
|
814
|
+
e.preventDefault()
|
|
815
|
+
setSelectedMentionIndex(
|
|
816
|
+
(prev) => (prev - 1 + mentionDisplayItems.length) % mentionDisplayItems.length
|
|
817
|
+
)
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
821
|
+
e.preventDefault()
|
|
822
|
+
const selected = mentionDisplayItems[selectedMentionIndex]
|
|
823
|
+
if (selected) {
|
|
824
|
+
selectMentionItem(selected)
|
|
825
|
+
}
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
if (e.key === 'Escape') {
|
|
829
|
+
e.preventDefault()
|
|
830
|
+
setShowMentionList(false)
|
|
831
|
+
setMentionCategory('all')
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
if (e.key === 'Backspace' && mentionCategory !== 'all' && mentionQuery === '') {
|
|
835
|
+
e.preventDefault()
|
|
836
|
+
setMentionCategory('all')
|
|
837
|
+
setSelectedMentionIndex(0)
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 3. Normal Send on Enter
|
|
843
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
844
|
+
e.preventDefault()
|
|
845
|
+
if (!isSendDisabled) {
|
|
846
|
+
handleFormSubmit()
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const isSendDisabled = !inputText.trim() && attachedFiles.length === 0
|
|
852
|
+
|
|
853
|
+
return (
|
|
854
|
+
<div
|
|
855
|
+
className={
|
|
856
|
+
className
|
|
857
|
+
? className
|
|
858
|
+
: inline
|
|
859
|
+
? 'w-full max-w-4xl mx-auto relative z-10'
|
|
860
|
+
: 'fixed bottom-8 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-50'
|
|
861
|
+
}
|
|
862
|
+
>
|
|
863
|
+
{/* File Attachment Pills Preview */}
|
|
864
|
+
{attachedFiles.length > 0 && (
|
|
865
|
+
<div className="mb-2 flex items-center gap-2 overflow-x-auto py-1 px-2 no-scrollbar animate-[holo-project-in_0.2s_ease-out_forwards]">
|
|
866
|
+
{attachedFiles.map((file, idx) => (
|
|
867
|
+
<div
|
|
868
|
+
key={file.path + idx}
|
|
869
|
+
className="flex items-center gap-2 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-full px-3 py-1.5 text-xs text-white shadow-lg animate-fade-in group hover:border-primary/50 transition-all flex-shrink-0"
|
|
870
|
+
>
|
|
871
|
+
{file.previewUrl ? (
|
|
872
|
+
<img
|
|
873
|
+
src={file.previewUrl}
|
|
874
|
+
alt={file.name}
|
|
875
|
+
className="w-4.5 h-4.5 rounded-full object-cover border border-white/20 shrink-0"
|
|
876
|
+
/>
|
|
877
|
+
) : (
|
|
878
|
+
<span className="text-sm shrink-0">{getFileIcon(file.name)}</span>
|
|
879
|
+
)}
|
|
880
|
+
<span className="max-w-[140px] truncate font-medium">{file.name}</span>
|
|
881
|
+
{file.size > 0 && (
|
|
882
|
+
<span className="text-[10px] text-white/40">{formatFileSize(file.size)}</span>
|
|
883
|
+
)}
|
|
884
|
+
<button
|
|
885
|
+
type="button"
|
|
886
|
+
onClick={() => removeFile(idx)}
|
|
887
|
+
className="text-white/40 hover:text-error hover:bg-error/20 p-1 rounded-full transition-all cursor-pointer"
|
|
888
|
+
title="Hapus Lampiran"
|
|
889
|
+
>
|
|
890
|
+
<FaTimes size={10} />
|
|
891
|
+
</button>
|
|
892
|
+
</div>
|
|
893
|
+
))}
|
|
894
|
+
</div>
|
|
895
|
+
)}
|
|
896
|
+
|
|
897
|
+
{/* Workspace Root Active Indicator Pill */}
|
|
898
|
+
{workspaceRoot && (
|
|
899
|
+
<div className="mb-2 flex items-center gap-2 px-3 py-1 bg-base-200/80 border border-primary/30 rounded-lg text-xs text-white/80 w-fit backdrop-blur-md animate-fade-in shadow-md">
|
|
900
|
+
<FaFolder className="text-primary text-xs" />
|
|
901
|
+
<span className="text-[10px] text-primary uppercase font-bold tracking-wider">
|
|
902
|
+
Workspace:
|
|
903
|
+
</span>
|
|
904
|
+
<span className="font-mono text-[11px] truncate max-w-xs">{workspaceRoot}</span>
|
|
905
|
+
{onSelectWorkspace && (
|
|
906
|
+
<button
|
|
907
|
+
type="button"
|
|
908
|
+
onClick={onSelectWorkspace}
|
|
909
|
+
className="ml-1 text-[10px] text-white/50 hover:text-primary transition-colors cursor-pointer underline"
|
|
910
|
+
title="Ganti folder proyek"
|
|
911
|
+
>
|
|
912
|
+
Ganti
|
|
913
|
+
</button>
|
|
914
|
+
)}
|
|
915
|
+
</div>
|
|
916
|
+
)}
|
|
917
|
+
|
|
918
|
+
{/* Hidden Native File Input */}
|
|
919
|
+
<input
|
|
920
|
+
ref={fileInputRef}
|
|
921
|
+
type="file"
|
|
922
|
+
multiple
|
|
923
|
+
className="hidden"
|
|
924
|
+
onChange={handleFileChange}
|
|
925
|
+
/>
|
|
926
|
+
|
|
927
|
+
<form
|
|
928
|
+
onSubmit={(e) => {
|
|
929
|
+
e.preventDefault()
|
|
930
|
+
handleFormSubmit()
|
|
931
|
+
}}
|
|
932
|
+
onDragOver={handleDragOver}
|
|
933
|
+
onDragLeave={handleDragLeave}
|
|
934
|
+
onDrop={handleDrop}
|
|
935
|
+
className={`relative flex items-center bg-[var(--glass-bg)] border rounded-lg p-2 pr-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)] transition-colors duration-300 focus-within:border-primary/50 focus-within:shadow-[0_0_20px_oklch(var(--p)/0.2)] ${
|
|
936
|
+
isDragging
|
|
937
|
+
? 'border-primary bg-primary/10 shadow-[0_0_30px_oklch(var(--p)/0.3)] scale-[1.02]'
|
|
938
|
+
: 'border-[var(--glass-border)]'
|
|
939
|
+
}`}
|
|
940
|
+
>
|
|
941
|
+
{/* Drag & Drop Overlay Indicator */}
|
|
942
|
+
{isDragging && (
|
|
943
|
+
<div className="absolute inset-0 rounded-lg bg-primary/20 backdrop-blur-md border-2 border-dashed border-primary flex items-center justify-center z-50 pointer-events-none text-white font-medium gap-2 animate-pulse">
|
|
944
|
+
<FaPaperclip className="animate-bounce" size={20} />
|
|
945
|
+
<span>Lepaskan file di sini untuk melampirkan...</span>
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
|
|
949
|
+
{/* Workspace Root Folder Button */}
|
|
950
|
+
{onSelectWorkspace && (
|
|
951
|
+
<button
|
|
952
|
+
type="button"
|
|
953
|
+
onClick={onSelectWorkspace}
|
|
954
|
+
className={`p-3 rounded-full transition-all flex-shrink-0 relative ${
|
|
955
|
+
workspaceRoot
|
|
956
|
+
? 'text-primary hover:text-primary hover:bg-primary/10'
|
|
957
|
+
: 'text-white/40 hover:text-white/80 hover:bg-white/5'
|
|
958
|
+
}`}
|
|
959
|
+
title={
|
|
960
|
+
workspaceRoot
|
|
961
|
+
? `Workspace Root: ${workspaceRoot} (Klik untuk ganti)`
|
|
962
|
+
: 'Atur Folder Proyek (Workspace Root)'
|
|
963
|
+
}
|
|
964
|
+
>
|
|
965
|
+
<FaFolder size={16} />
|
|
966
|
+
{workspaceRoot && (
|
|
967
|
+
<span className="absolute bottom-2 right-2 w-1.5 h-1.5 rounded-full bg-primary ring-1 ring-base-300" />
|
|
968
|
+
)}
|
|
969
|
+
</button>
|
|
970
|
+
)}
|
|
971
|
+
|
|
972
|
+
{/* Paperclip File Upload Button */}
|
|
973
|
+
<button
|
|
974
|
+
type="button"
|
|
975
|
+
onClick={handlePaperclipClick}
|
|
976
|
+
className="p-3 text-white/40 hover:text-white/80 hover:bg-white/5 rounded-full transition-all flex-shrink-0"
|
|
977
|
+
title="Lampirkan File (PDF, DOCX, TXT, MD, Gambar, dll)"
|
|
978
|
+
>
|
|
979
|
+
<FaPaperclip size={16} />
|
|
980
|
+
</button>
|
|
981
|
+
|
|
982
|
+
{/* Mic / Record Toggle (Hold to talk) */}
|
|
983
|
+
<button
|
|
984
|
+
type="button"
|
|
985
|
+
onClick={isRecording ? onStopRecord : onStartRecord}
|
|
986
|
+
disabled={isProcessing || isLoading}
|
|
987
|
+
className={`relative p-3 md:p-4 rounded-full flex-shrink-0 transition-all duration-300 transform outline-none z-10 ${
|
|
988
|
+
isProcessing
|
|
989
|
+
? 'text-primary bg-primary/20 cursor-wait'
|
|
990
|
+
: isLoading
|
|
991
|
+
? 'text-white/20 bg-white/5 cursor-not-allowed'
|
|
992
|
+
: isRecording
|
|
993
|
+
? 'text-error bg-error/20'
|
|
994
|
+
: 'text-white/40 hover:text-white/80 hover:bg-white/5'
|
|
995
|
+
}`}
|
|
996
|
+
style={{
|
|
997
|
+
transform: isRecording && !isProcessing ? `scale(${1 + audioIntensity * 0.3})` : '',
|
|
998
|
+
boxShadow:
|
|
999
|
+
isRecording && !isProcessing
|
|
1000
|
+
? `0 0 ${10 + audioIntensity * 40}px rgba(255,0,0, ${0.3 + audioIntensity * 0.5})`
|
|
1001
|
+
: ''
|
|
1002
|
+
}}
|
|
1003
|
+
title={
|
|
1004
|
+
isProcessing
|
|
1005
|
+
? 'Sedang memproses suara...'
|
|
1006
|
+
: isLoading
|
|
1007
|
+
? 'Agen sedang sibuk'
|
|
1008
|
+
: 'Mulai/Berhenti Rekam (Ctrl+Alt+M)'
|
|
1009
|
+
}
|
|
1010
|
+
>
|
|
1011
|
+
{isRecording && !isProcessing && (
|
|
1012
|
+
<div
|
|
1013
|
+
className="absolute inset-0 rounded-full bg-error/30 -z-10"
|
|
1014
|
+
style={{ transform: `scale(${1 + audioIntensity * 0.8})` }}
|
|
1015
|
+
/>
|
|
1016
|
+
)}
|
|
1017
|
+
{isProcessing ? (
|
|
1018
|
+
<span className="loading loading-spinner w-[18px] h-[18px]"></span>
|
|
1019
|
+
) : isLoading ? (
|
|
1020
|
+
<FaLock size={18} />
|
|
1021
|
+
) : (
|
|
1022
|
+
<FaMicrophone size={18} />
|
|
1023
|
+
)}
|
|
1024
|
+
</button>
|
|
1025
|
+
|
|
1026
|
+
{/* Emoji Button */}
|
|
1027
|
+
<div className="relative flex-shrink-0">
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
1031
|
+
className="p-3 text-white/40 hover:text-white/80 hover:bg-white/5 rounded-full transition-all"
|
|
1032
|
+
title="Insert Emoji"
|
|
1033
|
+
>
|
|
1034
|
+
<FaSmile size={18} />
|
|
1035
|
+
</button>
|
|
1036
|
+
|
|
1037
|
+
{showEmojiPicker && (
|
|
1038
|
+
<div className="absolute bottom-full left-0 mb-4 bg-[var(--glass-bg)] backdrop-blur-3xl border border-[var(--glass-border)] rounded-2xl p-2 shadow-2xl flex flex-wrap w-52 gap-1 z-[100] animate-[holo-project-in_0.2s_ease-out_forwards]">
|
|
1039
|
+
{EMOJIS.map((emoji) => (
|
|
1040
|
+
<button
|
|
1041
|
+
key={emoji}
|
|
1042
|
+
type="button"
|
|
1043
|
+
onClick={() => handleEmojiClick(emoji)}
|
|
1044
|
+
className="w-10 h-10 flex items-center justify-center hover:bg-white/10 rounded-xl text-2xl transition-all hover:scale-110 active:scale-95"
|
|
1045
|
+
>
|
|
1046
|
+
{emoji}
|
|
1047
|
+
</button>
|
|
1048
|
+
))}
|
|
1049
|
+
</div>
|
|
1050
|
+
)}
|
|
1051
|
+
</div>
|
|
1052
|
+
{/* Skill Autocomplete Dropdown */}
|
|
1053
|
+
{showSkillList && filteredSkills.length > 0 && (
|
|
1054
|
+
<div className="absolute bottom-full left-12 mb-2 w-[400px] bg-base-300/95 backdrop-blur-xl border border-[var(--glass-border)] rounded-xl overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.5)] z-50 animate-fade-in">
|
|
1055
|
+
<div className="p-2 text-xs font-bold text-gray-500 uppercase tracking-wider border-b border-white/5">
|
|
1056
|
+
Available Skills
|
|
1057
|
+
</div>
|
|
1058
|
+
<div ref={skillListRef} className="max-h-64 overflow-y-auto no-scrollbar">
|
|
1059
|
+
{filteredSkills.map((skillObj, idx) => (
|
|
1060
|
+
<div
|
|
1061
|
+
key={skillObj.name}
|
|
1062
|
+
onClick={() => selectSkill(skillObj)}
|
|
1063
|
+
className={`px-4 py-3 cursor-pointer transition-colors flex flex-col gap-1 border-b border-white/5 last:border-0 ${
|
|
1064
|
+
idx === selectedSkillIndex
|
|
1065
|
+
? 'bg-primary-500/20 text-primary-400'
|
|
1066
|
+
: 'hover:bg-white/10 text-gray-300'
|
|
1067
|
+
}`}
|
|
1068
|
+
>
|
|
1069
|
+
<div className="font-semibold text-sm">/{skillObj.name}</div>
|
|
1070
|
+
<div
|
|
1071
|
+
className={`text-xs ${idx === selectedSkillIndex ? 'text-primary-400/80' : 'text-gray-400'} line-clamp-2`}
|
|
1072
|
+
>
|
|
1073
|
+
{skillObj.description}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
))}
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
)}
|
|
1080
|
+
|
|
1081
|
+
{/* Mention (@) Autocomplete Dropdown */}
|
|
1082
|
+
{showMentionList && (mentionDisplayItems.length > 0 || isLoadingFiles) && (
|
|
1083
|
+
<div className="absolute bottom-full left-4 mb-2 w-[380px] max-w-[calc(100vw-2rem)] bg-base-300/95 backdrop-blur-xl border border-[var(--glass-border)] rounded-xl p-1.5 shadow-[0_8px_32px_rgba(0,0,0,0.5)] z-50 animate-fade-in flex flex-col">
|
|
1084
|
+
{/* Items List */}
|
|
1085
|
+
<div ref={mentionListRef} className="max-h-64 overflow-y-auto no-scrollbar">
|
|
1086
|
+
{mentionDisplayItems.length === 0 && !isLoadingFiles ? (
|
|
1087
|
+
<div className="py-3 text-center text-xs text-gray-400">Tidak ada yang cocok.</div>
|
|
1088
|
+
) : (
|
|
1089
|
+
mentionDisplayItems.map((item, idx) => (
|
|
1090
|
+
<div
|
|
1091
|
+
key={
|
|
1092
|
+
item.type === 'category'
|
|
1093
|
+
? item.category
|
|
1094
|
+
: item.type === 'file'
|
|
1095
|
+
? item.absolutePath
|
|
1096
|
+
: item.name
|
|
1097
|
+
}
|
|
1098
|
+
onClick={() => selectMentionItem(item)}
|
|
1099
|
+
className={`px-3 py-1.5 cursor-pointer transition-colors rounded-lg flex items-center gap-2.5 ${
|
|
1100
|
+
idx === selectedMentionIndex
|
|
1101
|
+
? 'bg-primary/20 text-primary'
|
|
1102
|
+
: 'hover:bg-white/5 text-gray-300'
|
|
1103
|
+
}`}
|
|
1104
|
+
>
|
|
1105
|
+
{/* Inline Icon */}
|
|
1106
|
+
<div className="shrink-0 text-sm opacity-80">
|
|
1107
|
+
{item.type === 'category' ? (
|
|
1108
|
+
item.category === 'files' ? (
|
|
1109
|
+
<Folder size={15} />
|
|
1110
|
+
) : (
|
|
1111
|
+
<Wrench size={15} />
|
|
1112
|
+
)
|
|
1113
|
+
) : item.type === 'file' ? (
|
|
1114
|
+
getFileIcon(item.name)
|
|
1115
|
+
) : (
|
|
1116
|
+
<Wrench size={15} />
|
|
1117
|
+
)}
|
|
1118
|
+
</div>
|
|
1119
|
+
|
|
1120
|
+
{/* Text Info */}
|
|
1121
|
+
<div className="flex-1 min-w-0">
|
|
1122
|
+
<div className="font-medium text-xs truncate">
|
|
1123
|
+
{item.type === 'category'
|
|
1124
|
+
? item.title
|
|
1125
|
+
: item.type === 'file'
|
|
1126
|
+
? item.relativePath
|
|
1127
|
+
: `@${item.name}`}
|
|
1128
|
+
</div>
|
|
1129
|
+
<div className="text-[11px] text-gray-400 truncate">
|
|
1130
|
+
{item.type === 'category'
|
|
1131
|
+
? item.subtitle
|
|
1132
|
+
: item.type === 'file'
|
|
1133
|
+
? formatFileSize(item.size)
|
|
1134
|
+
: item.description || item.label}
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
))
|
|
1139
|
+
)}
|
|
1140
|
+
</div>
|
|
1141
|
+
</div>
|
|
1142
|
+
)}
|
|
1143
|
+
|
|
1144
|
+
{/* Input Textarea */}
|
|
1145
|
+
<textarea
|
|
1146
|
+
ref={inputRef}
|
|
1147
|
+
rows={1}
|
|
1148
|
+
value={inputText}
|
|
1149
|
+
onChange={handleTextChange}
|
|
1150
|
+
onKeyDown={handleKeyDown}
|
|
1151
|
+
onPaste={handlePaste}
|
|
1152
|
+
placeholder={
|
|
1153
|
+
isLoading
|
|
1154
|
+
? 'Beri intervensi ke Mark...'
|
|
1155
|
+
: attachedFiles.length > 0
|
|
1156
|
+
? 'Tambah instruksi untuk file terlampir...'
|
|
1157
|
+
: 'Tanya apapun ke Mark...'
|
|
1158
|
+
}
|
|
1159
|
+
className="flex-1 resize-none bg-transparent border-none outline-none text-white px-3 py-2.5 text-sm md:text-base leading-normal placeholder:text-white/30 disabled:opacity-50 no-scrollbar"
|
|
1160
|
+
/>
|
|
1161
|
+
|
|
1162
|
+
{/* Action Buttons */}
|
|
1163
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
1164
|
+
{/* Auto Mode Toggle Button */}
|
|
1165
|
+
<button
|
|
1166
|
+
type="button"
|
|
1167
|
+
onClick={handleToggleAutoMode}
|
|
1168
|
+
className={`relative flex items-center justify-center gap-1 px-2.5 py-1 rounded-full text-xs font-mono transition-all duration-200 cursor-pointer select-none outline-none btn btn-circle ${
|
|
1169
|
+
isAutoMode
|
|
1170
|
+
? 'bg-warning/20 text-warning border border-warning/40 font-semibold'
|
|
1171
|
+
: 'text-white/30 hover:text-white/70 hover:bg-white/5 border border-transparent font-normal'
|
|
1172
|
+
}`}
|
|
1173
|
+
title={
|
|
1174
|
+
isAutoMode
|
|
1175
|
+
? 'Auto Mode: AKTIF. Semua persetujuan tool otomatis diizinkan untuk sesi ini.'
|
|
1176
|
+
: 'Auto Mode: NONAKTIF. Klik untuk otomatis menyetujui semua tool di sesi ini.'
|
|
1177
|
+
}
|
|
1178
|
+
>
|
|
1179
|
+
<Zap className={`w-3 h-3 ${isAutoMode ? 'fill-white text-white' : ''}`} />
|
|
1180
|
+
</button>
|
|
1181
|
+
|
|
1182
|
+
{/* Ring Gauge Context Indicator (~30px) */}
|
|
1183
|
+
{(() => {
|
|
1184
|
+
const pct = Math.min(100, Math.max(0, contextTracker.percentage || 0))
|
|
1185
|
+
const roundedPct =
|
|
1186
|
+
(contextTracker.currentTokens || contextTracker.currentChars || 0) > 0
|
|
1187
|
+
? Math.max(1, Math.round(pct))
|
|
1188
|
+
: Math.round(pct)
|
|
1189
|
+
const radius = 14
|
|
1190
|
+
const circumference = 2 * Math.PI * radius
|
|
1191
|
+
const strokeDashoffset = circumference - (pct / 100) * circumference
|
|
1192
|
+
const colorClass =
|
|
1193
|
+
pct >= 85 ? 'stroke-rose-500' : pct >= 50 ? 'stroke-amber-400' : 'stroke-emerald-400'
|
|
1194
|
+
const barColorClass =
|
|
1195
|
+
pct >= 85 ? 'bg-rose-500' : pct >= 50 ? 'bg-amber-400' : 'bg-emerald-400'
|
|
1196
|
+
|
|
1197
|
+
return (
|
|
1198
|
+
<div className="relative flex items-center justify-center px-1 select-none">
|
|
1199
|
+
{/* Ring Gauge Trigger Button */}
|
|
1200
|
+
<div
|
|
1201
|
+
ref={gaugeRef}
|
|
1202
|
+
role="button"
|
|
1203
|
+
tabIndex={0}
|
|
1204
|
+
onClick={() => setShowContextPopover((prev) => !prev)}
|
|
1205
|
+
onKeyDown={(e) => {
|
|
1206
|
+
if (e.key === 'Enter' || e.key === ' ') setShowContextPopover((prev) => !prev)
|
|
1207
|
+
}}
|
|
1208
|
+
className="relative w-[30px] h-[30px] flex items-center justify-center cursor-pointer transition-transform duration-200 hover:scale-110 group/gauge outline-none"
|
|
1209
|
+
title="Context Window Info (Dual-Layer Hermes Compressor)"
|
|
1210
|
+
>
|
|
1211
|
+
<svg className="w-full h-full -rotate-90" viewBox="0 0 36 36">
|
|
1212
|
+
<circle
|
|
1213
|
+
cx="18"
|
|
1214
|
+
cy="18"
|
|
1215
|
+
r={radius}
|
|
1216
|
+
className="stroke-white/10"
|
|
1217
|
+
strokeWidth="2.5"
|
|
1218
|
+
fill="none"
|
|
1219
|
+
/>
|
|
1220
|
+
{pct > 0 && (
|
|
1221
|
+
<circle
|
|
1222
|
+
cx="18"
|
|
1223
|
+
cy="18"
|
|
1224
|
+
r={radius}
|
|
1225
|
+
className={`${colorClass} transition-all duration-500 ease-out`}
|
|
1226
|
+
strokeWidth="2.5"
|
|
1227
|
+
strokeDasharray={circumference}
|
|
1228
|
+
strokeDashoffset={strokeDashoffset}
|
|
1229
|
+
strokeLinecap="round"
|
|
1230
|
+
fill="none"
|
|
1231
|
+
/>
|
|
1232
|
+
)}
|
|
1233
|
+
</svg>
|
|
1234
|
+
<span className="absolute inset-0 flex items-center justify-center font-mono text-[8px] font-bold text-white/80 group-hover/gauge:text-white tracking-tight leading-none pointer-events-none">
|
|
1235
|
+
{roundedPct}%
|
|
1236
|
+
</span>
|
|
1237
|
+
</div>
|
|
1238
|
+
|
|
1239
|
+
{/* Popover Card Overlay */}
|
|
1240
|
+
{showContextPopover && (
|
|
1241
|
+
<div
|
|
1242
|
+
ref={contextPopoverRef}
|
|
1243
|
+
className="absolute bottom-full right-0 mb-3.5 w-64 p-3.5 bg-base-200 border border-primary/30 rounded-xl shadow-2xl z-50 animate-fade-in text-left cursor-default"
|
|
1244
|
+
onClick={(e) => e.stopPropagation()}
|
|
1245
|
+
>
|
|
1246
|
+
<div className="flex items-center justify-between text-[11px] font-medium text-white/40 mb-0.5">
|
|
1247
|
+
<span>Session Info</span>
|
|
1248
|
+
<span className="text-[9px] uppercase tracking-wider text-primary font-bold">
|
|
1249
|
+
In-Place
|
|
1250
|
+
</span>
|
|
1251
|
+
</div>
|
|
1252
|
+
<div className="text-xs font-bold text-white mb-2.5">Context Window</div>
|
|
1253
|
+
|
|
1254
|
+
<div className="flex items-center justify-between text-xs font-semibold mb-1.5 font-mono">
|
|
1255
|
+
<span className="text-white">
|
|
1256
|
+
{(contextTracker.currentTokens || contextTracker.currentChars || 0) >= 1000
|
|
1257
|
+
? `${((contextTracker.currentTokens || contextTracker.currentChars || 0) / 1000).toFixed(1)}K`
|
|
1258
|
+
: contextTracker.currentTokens || contextTracker.currentChars || 0}{' '}
|
|
1259
|
+
/ 256K tokens
|
|
1260
|
+
</span>
|
|
1261
|
+
<span className="text-white/60">{roundedPct}%</span>
|
|
1262
|
+
</div>
|
|
1263
|
+
|
|
1264
|
+
{/* Progress Bar */}
|
|
1265
|
+
<div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden mb-1.5">
|
|
1266
|
+
<div
|
|
1267
|
+
className={`h-full ${barColorClass} transition-all duration-300 rounded-full`}
|
|
1268
|
+
style={{ width: `${Math.min(100, Math.max(pct, 2))}%` }}
|
|
1269
|
+
/>
|
|
1270
|
+
</div>
|
|
1271
|
+
|
|
1272
|
+
<div className="text-[10px] text-white/40 mb-2 font-mono">
|
|
1273
|
+
{pct >= 85
|
|
1274
|
+
? 'Gateway Hygiene Active (>=85%)'
|
|
1275
|
+
: pct >= 50
|
|
1276
|
+
? 'In-Loop Compressor Guard (>=50%)'
|
|
1277
|
+
: 'Optimal Context Load'}
|
|
1278
|
+
</div>
|
|
1279
|
+
|
|
1280
|
+
{/* Compact Conversation Button */}
|
|
1281
|
+
<button
|
|
1282
|
+
type="button"
|
|
1283
|
+
onClick={() => {
|
|
1284
|
+
setShowContextPopover(false)
|
|
1285
|
+
if (typeof onManualCompact === 'function') {
|
|
1286
|
+
onManualCompact()
|
|
1287
|
+
} else {
|
|
1288
|
+
window.dispatchEvent(new CustomEvent('request-manual-compaction'))
|
|
1289
|
+
}
|
|
1290
|
+
}}
|
|
1291
|
+
disabled={isLoading}
|
|
1292
|
+
className="w-full py-2 px-3 rounded-lg bg-white/5 hover:bg-white/10 active:bg-white/15 border border-primary/30 text-xs font-medium text-white transition-all flex items-center justify-center gap-1.5 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1293
|
+
>
|
|
1294
|
+
<span>Compact Conversation</span>
|
|
1295
|
+
</button>
|
|
1296
|
+
|
|
1297
|
+
{/* Caret pointing to gauge */}
|
|
1298
|
+
<div className="absolute -bottom-1.5 right-3.5 w-3 h-3 bg-base-200 border-r border-b border-primary/30 rotate-45" />
|
|
1299
|
+
</div>
|
|
1300
|
+
)}
|
|
1301
|
+
</div>
|
|
1302
|
+
)
|
|
1303
|
+
})()}
|
|
1304
|
+
|
|
1305
|
+
{isLoading && (
|
|
1306
|
+
<button
|
|
1307
|
+
type="button"
|
|
1308
|
+
onClick={() => setShowAbortConfirm(true)}
|
|
1309
|
+
className="p-3 rounded-full bg-error/20 text-error hover:bg-error hover:text-white transition-all"
|
|
1310
|
+
title="Stop Generation (Hard Abort)"
|
|
1311
|
+
>
|
|
1312
|
+
<FaStop size={16} />
|
|
1313
|
+
</button>
|
|
1314
|
+
)}
|
|
1315
|
+
<button
|
|
1316
|
+
type="submit"
|
|
1317
|
+
disabled={isSendDisabled}
|
|
1318
|
+
className="p-3 rounded-full bg-success text-success-content disabled:opacity-30 disabled:bg-white/10 disabled:text-white/30 hover:bg-success/80 hover:scale-105 active:scale-95 transition-all"
|
|
1319
|
+
title="Send Message"
|
|
1320
|
+
>
|
|
1321
|
+
<FaArrowUp size={16} />
|
|
1322
|
+
</button>
|
|
1323
|
+
</div>
|
|
1324
|
+
</form>
|
|
1325
|
+
|
|
1326
|
+
<ConfirmModal
|
|
1327
|
+
isOpen={showAbortConfirm}
|
|
1328
|
+
title="Hard Abort Proses?"
|
|
1329
|
+
message="Yakin mau memberhentikan proses Mark secara paksa? Tindakan ini akan menghentikan secara langsung semua alat yang sedang berjalan dan memutuskan koneksi ke otak AI-nya seketika."
|
|
1330
|
+
confirmText="Berhentikan"
|
|
1331
|
+
cancelText="Batal"
|
|
1332
|
+
isError={true}
|
|
1333
|
+
onConfirm={() => {
|
|
1334
|
+
setShowAbortConfirm(false)
|
|
1335
|
+
if (lastPromptRef.current) {
|
|
1336
|
+
setInputText(lastPromptRef.current)
|
|
1337
|
+
}
|
|
1338
|
+
if (onStop) onStop()
|
|
1339
|
+
}}
|
|
1340
|
+
onCancel={() => setShowAbortConfirm(false)}
|
|
1341
|
+
/>
|
|
1342
|
+
</div>
|
|
1343
|
+
)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
export default memo(InputBar)
|