@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.
Files changed (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. 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">&#x2058;</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
+ }