@kirosnn/mosaic 0.0.7
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/.mosaic/mosaic.local.jsonc +0 -0
- package/MOSAIC.md +188 -0
- package/README.md +127 -0
- package/docs/mosaic.png +0 -0
- package/package.json +42 -0
- package/src/agent/Agent.ts +131 -0
- package/src/agent/context.ts +96 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/prompts/systemPrompt.ts +138 -0
- package/src/agent/prompts/toolsPrompt.ts +139 -0
- package/src/agent/provider/anthropic.ts +122 -0
- package/src/agent/provider/google.ts +124 -0
- package/src/agent/provider/mistral.ts +117 -0
- package/src/agent/provider/ollama.ts +531 -0
- package/src/agent/provider/openai.ts +220 -0
- package/src/agent/provider/xai.ts +122 -0
- package/src/agent/tools/bash.ts +20 -0
- package/src/agent/tools/definitions.ts +27 -0
- package/src/agent/tools/edit.ts +23 -0
- package/src/agent/tools/executor.ts +751 -0
- package/src/agent/tools/explore.ts +18 -0
- package/src/agent/tools/exploreExecutor.ts +320 -0
- package/src/agent/tools/glob.ts +16 -0
- package/src/agent/tools/grep.ts +19 -0
- package/src/agent/tools/index.ts +4 -0
- package/src/agent/tools/list.ts +20 -0
- package/src/agent/tools/question.ts +20 -0
- package/src/agent/tools/read.ts +15 -0
- package/src/agent/tools/write.ts +21 -0
- package/src/agent/types.ts +155 -0
- package/src/components/App.tsx +174 -0
- package/src/components/CommandsModal.tsx +77 -0
- package/src/components/CustomInput.tsx +328 -0
- package/src/components/Main.tsx +1112 -0
- package/src/components/Notification.tsx +91 -0
- package/src/components/SelectList.tsx +47 -0
- package/src/components/Setup.tsx +528 -0
- package/src/components/ShortcutsModal.tsx +67 -0
- package/src/components/Welcome.tsx +39 -0
- package/src/components/main/ApprovalPanel.tsx +134 -0
- package/src/components/main/ChatPage.tsx +516 -0
- package/src/components/main/HomePage.tsx +111 -0
- package/src/components/main/QuestionPanel.tsx +85 -0
- package/src/components/main/ThinkingIndicator.tsx +101 -0
- package/src/components/main/types.ts +55 -0
- package/src/components/main/wrapText.ts +41 -0
- package/src/index.tsx +212 -0
- package/src/utils/approvalBridge.ts +129 -0
- package/src/utils/commands/echo.ts +22 -0
- package/src/utils/commands/help.ts +25 -0
- package/src/utils/commands/index.ts +68 -0
- package/src/utils/commands/init.ts +68 -0
- package/src/utils/commands/redo.ts +74 -0
- package/src/utils/commands/registry.ts +29 -0
- package/src/utils/commands/sessions.ts +129 -0
- package/src/utils/commands/types.ts +20 -0
- package/src/utils/commands/undo.ts +75 -0
- package/src/utils/commands/web.ts +77 -0
- package/src/utils/config.ts +357 -0
- package/src/utils/diff.ts +201 -0
- package/src/utils/diffRendering.tsx +62 -0
- package/src/utils/exploreBridge.ts +87 -0
- package/src/utils/fileChangeTracker.ts +98 -0
- package/src/utils/fileChangesBridge.ts +18 -0
- package/src/utils/history.ts +106 -0
- package/src/utils/markdown.tsx +232 -0
- package/src/utils/models.ts +304 -0
- package/src/utils/questionBridge.ts +122 -0
- package/src/utils/terminalUtils.ts +25 -0
- package/src/utils/toolFormatting.ts +384 -0
- package/src/utils/undoRedo.ts +429 -0
- package/src/utils/undoRedoBridge.ts +45 -0
- package/src/utils/undoRedoDb.ts +338 -0
- package/src/utils/uninstall.ts +45 -0
- package/src/utils/version.ts +3 -0
- package/src/web/app.tsx +606 -0
- package/src/web/assets/css/ChatPage.css +212 -0
- package/src/web/assets/css/FileExplorer.css +202 -0
- package/src/web/assets/css/HomePage.css +119 -0
- package/src/web/assets/css/Markdown.css +178 -0
- package/src/web/assets/css/MessageItem.css +160 -0
- package/src/web/assets/css/Sidebar.css +208 -0
- package/src/web/assets/css/SidebarModal.css +137 -0
- package/src/web/assets/css/ThinkingIndicator.css +47 -0
- package/src/web/assets/css/ToolMessage.css +148 -0
- package/src/web/assets/css/global.css +226 -0
- package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
- package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
- package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
- package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
- package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
- package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
- package/src/web/assets/images/favicon-v2.svg +6 -0
- package/src/web/assets/images/favicon.png +0 -0
- package/src/web/assets/images/foruse.svg +5 -0
- package/src/web/assets/images/logo_black.svg +5 -0
- package/src/web/assets/images/logo_white.svg +5 -0
- package/src/web/assets/images/logoblack.png +0 -0
- package/src/web/assets/images/logowhite.png +0 -0
- package/src/web/build.ts +23 -0
- package/src/web/components/ApprovalPanel.tsx +191 -0
- package/src/web/components/ChatPage.tsx +273 -0
- package/src/web/components/FileExplorer.tsx +162 -0
- package/src/web/components/HomePage.tsx +121 -0
- package/src/web/components/MessageItem.tsx +178 -0
- package/src/web/components/Modal.tsx +30 -0
- package/src/web/components/QuestionPanel.tsx +149 -0
- package/src/web/components/Setup.tsx +211 -0
- package/src/web/components/Sidebar.tsx +292 -0
- package/src/web/components/ThinkingIndicator.tsx +85 -0
- package/src/web/logo_black.svg +5 -0
- package/src/web/logo_white.svg +5 -0
- package/src/web/router.ts +46 -0
- package/src/web/server.tsx +662 -0
- package/src/web/storage.ts +92 -0
- package/src/web/types.ts +17 -0
- package/src/web/utils.ts +61 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Conversation, formatWorkspace } from '../storage';
|
|
4
|
+
|
|
5
|
+
type TimePeriod = 'today' | 'yesterday' | 'previous7days' | 'previous30days' | 'older';
|
|
6
|
+
|
|
7
|
+
interface GroupedConversations {
|
|
8
|
+
period: TimePeriod;
|
|
9
|
+
label: string;
|
|
10
|
+
conversations: Conversation[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getTimePeriod(timestamp: number): TimePeriod {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const date = new Date(timestamp);
|
|
16
|
+
|
|
17
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
18
|
+
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
|
19
|
+
const startOf7DaysAgo = startOfToday - 7 * 24 * 60 * 60 * 1000;
|
|
20
|
+
const startOf30DaysAgo = startOfToday - 30 * 24 * 60 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
if (timestamp >= startOfToday) {
|
|
23
|
+
return 'today';
|
|
24
|
+
} else if (timestamp >= startOfYesterday) {
|
|
25
|
+
return 'yesterday';
|
|
26
|
+
} else if (timestamp >= startOf7DaysAgo) {
|
|
27
|
+
return 'previous7days';
|
|
28
|
+
} else if (timestamp >= startOf30DaysAgo) {
|
|
29
|
+
return 'previous30days';
|
|
30
|
+
} else {
|
|
31
|
+
return 'older';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const periodLabels: Record<TimePeriod, string> = {
|
|
36
|
+
today: 'Today',
|
|
37
|
+
yesterday: 'Yesterday',
|
|
38
|
+
previous7days: 'Previous 7 days',
|
|
39
|
+
previous30days: 'Previous 30 days',
|
|
40
|
+
older: 'Older',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const periodOrder: TimePeriod[] = ['today', 'yesterday', 'previous7days', 'previous30days', 'older'];
|
|
44
|
+
|
|
45
|
+
function groupConversationsByPeriod(conversations: Conversation[]): GroupedConversations[] {
|
|
46
|
+
const groups: Record<TimePeriod, Conversation[]> = {
|
|
47
|
+
today: [],
|
|
48
|
+
yesterday: [],
|
|
49
|
+
previous7days: [],
|
|
50
|
+
previous30days: [],
|
|
51
|
+
older: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const conv of conversations) {
|
|
55
|
+
const period = getTimePeriod(conv.updatedAt);
|
|
56
|
+
groups[period].push(conv);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return periodOrder
|
|
60
|
+
.filter(period => groups[period].length > 0)
|
|
61
|
+
.map(period => ({
|
|
62
|
+
period,
|
|
63
|
+
label: periodLabels[period],
|
|
64
|
+
conversations: groups[period],
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SidebarProps {
|
|
69
|
+
isExpanded: boolean;
|
|
70
|
+
onToggleExpand: () => void;
|
|
71
|
+
onNavigateToNewChat: () => void;
|
|
72
|
+
onNavigateHome?: () => void;
|
|
73
|
+
onOpenSettings: () => void;
|
|
74
|
+
onOpenHelp: () => void;
|
|
75
|
+
conversations?: Conversation[];
|
|
76
|
+
currentConversationId?: string | null;
|
|
77
|
+
onLoadConversation?: (id: string) => void;
|
|
78
|
+
onDeleteConversation?: (id: string) => void;
|
|
79
|
+
onRenameConversation?: (id: string, newTitle: string) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function Sidebar({
|
|
83
|
+
isExpanded,
|
|
84
|
+
onToggleExpand,
|
|
85
|
+
onNavigateToNewChat,
|
|
86
|
+
onNavigateHome,
|
|
87
|
+
onOpenSettings,
|
|
88
|
+
onOpenHelp,
|
|
89
|
+
conversations = [],
|
|
90
|
+
currentConversationId,
|
|
91
|
+
onLoadConversation,
|
|
92
|
+
onDeleteConversation,
|
|
93
|
+
onRenameConversation
|
|
94
|
+
}: SidebarProps) {
|
|
95
|
+
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
96
|
+
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
|
97
|
+
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
98
|
+
const [editTargetId, setEditTargetId] = useState<string | null>(null);
|
|
99
|
+
const [editTitle, setEditTitle] = useState('');
|
|
100
|
+
|
|
101
|
+
const handleDeleteClick = (e: React.MouseEvent, id: string) => {
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
setDeleteTargetId(id);
|
|
104
|
+
setDeleteModalOpen(true);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleConfirmDelete = () => {
|
|
108
|
+
if (deleteTargetId) {
|
|
109
|
+
onDeleteConversation?.(deleteTargetId);
|
|
110
|
+
}
|
|
111
|
+
setDeleteModalOpen(false);
|
|
112
|
+
setDeleteTargetId(null);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleCancelDelete = () => {
|
|
116
|
+
setDeleteModalOpen(false);
|
|
117
|
+
setDeleteTargetId(null);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleEditClick = (e: React.MouseEvent, conv: Conversation) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
setEditTargetId(conv.id);
|
|
123
|
+
setEditTitle(conv.title || '');
|
|
124
|
+
setEditModalOpen(true);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleConfirmEdit = () => {
|
|
128
|
+
if (editTargetId && editTitle.trim()) {
|
|
129
|
+
onRenameConversation?.(editTargetId, editTitle.trim());
|
|
130
|
+
}
|
|
131
|
+
setEditModalOpen(false);
|
|
132
|
+
setEditTargetId(null);
|
|
133
|
+
setEditTitle('');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleCancelEdit = () => {
|
|
137
|
+
setEditModalOpen(false);
|
|
138
|
+
setEditTargetId(null);
|
|
139
|
+
setEditTitle('');
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
|
143
|
+
if (e.key === 'Enter') {
|
|
144
|
+
handleConfirmEdit();
|
|
145
|
+
} else if (e.key === 'Escape') {
|
|
146
|
+
handleCancelEdit();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const groupedConversations = useMemo(
|
|
151
|
+
() => groupConversationsByPeriod(conversations),
|
|
152
|
+
[conversations]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<div className={`sidebar ${isExpanded ? 'expanded' : ''}`}>
|
|
158
|
+
<div className="sidebar-top">
|
|
159
|
+
<button className="icon-btn" onClick={onToggleExpand} title={isExpanded ? "Collapse" : "Expand"}>
|
|
160
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
|
|
161
|
+
<span className="label">Collapse</span>
|
|
162
|
+
</button>
|
|
163
|
+
<button className="icon-btn" onClick={onNavigateHome} title="Home">
|
|
164
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
|
165
|
+
<span className="label">Home</span>
|
|
166
|
+
</button>
|
|
167
|
+
<button className="icon-btn" onClick={onNavigateToNewChat} title="New Chat">
|
|
168
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
|
169
|
+
<span className="label">New Chat</span>
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{isExpanded && conversations.length > 0 && (
|
|
174
|
+
<div className="sidebar-conversations">
|
|
175
|
+
<div className="conversations-list">
|
|
176
|
+
{groupedConversations.map((group) => (
|
|
177
|
+
<div key={group.period} className="conversation-group">
|
|
178
|
+
<div className="conversation-group-header">{group.label}</div>
|
|
179
|
+
{group.conversations.map((conv) => (
|
|
180
|
+
<div
|
|
181
|
+
key={conv.id}
|
|
182
|
+
className={`conversation-item ${conv.id === currentConversationId ? 'active' : ''}`}
|
|
183
|
+
onClick={() => onLoadConversation?.(conv.id)}
|
|
184
|
+
>
|
|
185
|
+
<div className="conversation-info">
|
|
186
|
+
<span className="conversation-title">
|
|
187
|
+
{conv.title || 'New conversation'}
|
|
188
|
+
</span>
|
|
189
|
+
{conv.workspace && (
|
|
190
|
+
<span className="conversation-workspace" title={conv.workspace}>
|
|
191
|
+
{formatWorkspace(conv.workspace)}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="conversation-actions">
|
|
196
|
+
<button
|
|
197
|
+
className="conversation-action-btn conversation-edit"
|
|
198
|
+
onClick={(e) => handleEditClick(e, conv)}
|
|
199
|
+
title="Rename"
|
|
200
|
+
>
|
|
201
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
202
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
203
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
className="conversation-action-btn conversation-delete"
|
|
208
|
+
onClick={(e) => handleDeleteClick(e, conv.id)}
|
|
209
|
+
title="Delete"
|
|
210
|
+
>
|
|
211
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
212
|
+
<polyline points="3 6 5 6 21 6"></polyline>
|
|
213
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
214
|
+
</svg>
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
<div className="sidebar-bottom">
|
|
226
|
+
<button className="icon-btn" onClick={onOpenSettings} title="Settings">
|
|
227
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
228
|
+
<span className="label">Settings</span>
|
|
229
|
+
</button>
|
|
230
|
+
<button className="icon-btn" onClick={onOpenHelp} title="Help">
|
|
231
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
232
|
+
<span className="label">Help</span>
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{deleteModalOpen && (
|
|
238
|
+
<div className="sidebar-modal-overlay" onClick={handleCancelDelete}>
|
|
239
|
+
<div className="sidebar-modal" onClick={(e) => e.stopPropagation()}>
|
|
240
|
+
<div className="sidebar-modal-header">
|
|
241
|
+
<h3>Delete conversation</h3>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="sidebar-modal-body">
|
|
244
|
+
<p>Are you sure you want to delete this conversation? This action cannot be undone.</p>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="sidebar-modal-actions">
|
|
247
|
+
<button className="sidebar-modal-btn cancel" onClick={handleCancelDelete}>
|
|
248
|
+
Cancel
|
|
249
|
+
</button>
|
|
250
|
+
<button className="sidebar-modal-btn delete" onClick={handleConfirmDelete}>
|
|
251
|
+
Delete
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{editModalOpen && (
|
|
259
|
+
<div className="sidebar-modal-overlay" onClick={handleCancelEdit}>
|
|
260
|
+
<div className="sidebar-modal" onClick={(e) => e.stopPropagation()}>
|
|
261
|
+
<div className="sidebar-modal-header">
|
|
262
|
+
<h3>Rename conversation</h3>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="sidebar-modal-body">
|
|
265
|
+
<input
|
|
266
|
+
type="text"
|
|
267
|
+
className="sidebar-modal-input"
|
|
268
|
+
value={editTitle}
|
|
269
|
+
onChange={(e) => setEditTitle(e.target.value)}
|
|
270
|
+
onKeyDown={handleEditKeyDown}
|
|
271
|
+
placeholder="Conversation title"
|
|
272
|
+
autoFocus
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="sidebar-modal-actions">
|
|
276
|
+
<button className="sidebar-modal-btn cancel" onClick={handleCancelEdit}>
|
|
277
|
+
Cancel
|
|
278
|
+
</button>
|
|
279
|
+
<button
|
|
280
|
+
className="sidebar-modal-btn confirm"
|
|
281
|
+
onClick={handleConfirmEdit}
|
|
282
|
+
disabled={!editTitle.trim()}
|
|
283
|
+
>
|
|
284
|
+
Save
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import '../assets/css/ThinkingIndicator.css';
|
|
4
|
+
|
|
5
|
+
const THINKING_WORDS = [
|
|
6
|
+
"Thinking",
|
|
7
|
+
"Processing",
|
|
8
|
+
"Analyzing",
|
|
9
|
+
"Reasoning",
|
|
10
|
+
"Computing",
|
|
11
|
+
"Pondering",
|
|
12
|
+
"Crafting",
|
|
13
|
+
"Working",
|
|
14
|
+
"Brewing",
|
|
15
|
+
"Weaving",
|
|
16
|
+
"Revolutionizing"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
interface ThinkingIndicatorProps {
|
|
20
|
+
startTime?: number;
|
|
21
|
+
tokens?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatElapsedTime(startTime: number | undefined): string {
|
|
25
|
+
if (!startTime) return "";
|
|
26
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
27
|
+
const hours = Math.floor(elapsed / 3600);
|
|
28
|
+
const minutes = Math.floor((elapsed % 3600) / 60);
|
|
29
|
+
const seconds = elapsed % 60;
|
|
30
|
+
if (hours > 0) {
|
|
31
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
32
|
+
}
|
|
33
|
+
if (minutes > 0) {
|
|
34
|
+
return `${minutes}m ${seconds}s`;
|
|
35
|
+
}
|
|
36
|
+
return `${seconds}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ThinkingIndicator({ startTime, tokens }: ThinkingIndicatorProps) {
|
|
40
|
+
const [shimmerPos, setShimmerPos] = useState(-2);
|
|
41
|
+
const [, setTick] = useState(0);
|
|
42
|
+
const thinkingWord = useMemo(
|
|
43
|
+
() => THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)],
|
|
44
|
+
[]
|
|
45
|
+
);
|
|
46
|
+
const text = `${thinkingWord}...`;
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const interval = setInterval(() => {
|
|
50
|
+
setShimmerPos((prev) => {
|
|
51
|
+
const limit = text.length + 20;
|
|
52
|
+
return prev >= limit ? -2 : prev + 1;
|
|
53
|
+
});
|
|
54
|
+
setTick((prev) => prev + 1);
|
|
55
|
+
}, 50);
|
|
56
|
+
|
|
57
|
+
return () => clearInterval(interval);
|
|
58
|
+
}, [text.length]);
|
|
59
|
+
|
|
60
|
+
const elapsedStr = formatElapsedTime(startTime);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="thinking-indicator">
|
|
64
|
+
<span className="thinking-icon">⁘</span>
|
|
65
|
+
<span className="thinking-text">
|
|
66
|
+
{text.split("").map((char, index) => {
|
|
67
|
+
const inShimmer = index === shimmerPos || index === shimmerPos - 1;
|
|
68
|
+
return (
|
|
69
|
+
<span
|
|
70
|
+
key={index}
|
|
71
|
+
className={inShimmer ? "shimmer-active" : "shimmer-dim"}
|
|
72
|
+
>
|
|
73
|
+
{char}
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</span>
|
|
78
|
+
{elapsedStr && <span className="thinking-elapsed"> - {elapsedStr}</span>}
|
|
79
|
+
<span className="thinking-hint"> - esc to cancel</span>
|
|
80
|
+
{tokens !== undefined && tokens > 0 && (
|
|
81
|
+
<span className="thinking-tokens"> - {tokens.toLocaleString()} tokens</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" ?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" viewBox="0 0 100 100">
|
|
3
|
+
<path d="M10 40 C 20 20, 40 20, 50 40 S 80 60, 90 40" fill="black"/>
|
|
4
|
+
<path d="M10 60 C 20 40, 40 40, 50 60 S 80 80, 90 60" fill="black"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" ?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" viewBox="0 0 100 100">
|
|
3
|
+
<path d="M10 40 C 20 20, 40 20, 50 40 S 80 60, 90 40" fill="white"/>
|
|
4
|
+
<path d="M10 60 C 20 40, 40 40, 50 60 S 80 80, 90 60" fill="white"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type Route =
|
|
2
|
+
| { page: 'home' }
|
|
3
|
+
| { page: 'chat'; conversationId: string | null };
|
|
4
|
+
|
|
5
|
+
export function parseRoute(pathname: string): Route {
|
|
6
|
+
if (pathname === '/' || pathname === '/home') {
|
|
7
|
+
return { page: 'home' };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (pathname === '/chat' || pathname === '/chat/new') {
|
|
11
|
+
return { page: 'chat', conversationId: null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const chatMatch = pathname.match(/^\/chat\/(.+)$/);
|
|
15
|
+
if (chatMatch && chatMatch[1]) {
|
|
16
|
+
return { page: 'chat', conversationId: chatMatch[1] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { page: 'home' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPath(route: Route): string {
|
|
23
|
+
if (route.page === 'home') {
|
|
24
|
+
return '/';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (route.page === 'chat') {
|
|
28
|
+
if (route.conversationId) {
|
|
29
|
+
return `/chat/${route.conversationId}`;
|
|
30
|
+
}
|
|
31
|
+
return '/chat/new';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return '/';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function navigateTo(route: Route): void {
|
|
38
|
+
const path = buildPath(route);
|
|
39
|
+
window.history.pushState(null, '', path);
|
|
40
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function replaceTo(route: Route): void {
|
|
44
|
+
const path = buildPath(route);
|
|
45
|
+
window.history.replaceState(null, '', path);
|
|
46
|
+
}
|