@chaaskit/client 0.1.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.
Files changed (135) hide show
  1. package/dist/favicon.svg +11 -0
  2. package/dist/index.html +17 -0
  3. package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
  4. package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
  5. package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
  6. package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
  7. package/dist/lib/extensions.js +10 -0
  8. package/dist/lib/extensions.js.map +1 -0
  9. package/dist/lib/favicon.svg +11 -0
  10. package/dist/lib/index.js +74126 -0
  11. package/dist/lib/index.js.map +1 -0
  12. package/dist/lib/logo.svg +12 -0
  13. package/dist/lib/routes/AcceptInviteRoute.js +19 -0
  14. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
  15. package/dist/lib/routes/AdminDashboardRoute.js +19 -0
  16. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
  17. package/dist/lib/routes/AdminTeamRoute.js +19 -0
  18. package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
  19. package/dist/lib/routes/AdminTeamsRoute.js +19 -0
  20. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
  21. package/dist/lib/routes/AdminUsersRoute.js +19 -0
  22. package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
  23. package/dist/lib/routes/ApiKeysRoute.js +19 -0
  24. package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
  25. package/dist/lib/routes/AutomationsRoute.js +19 -0
  26. package/dist/lib/routes/AutomationsRoute.js.map +1 -0
  27. package/dist/lib/routes/ChatRoute.js +19 -0
  28. package/dist/lib/routes/ChatRoute.js.map +1 -0
  29. package/dist/lib/routes/DocumentsRoute.js +19 -0
  30. package/dist/lib/routes/DocumentsRoute.js.map +1 -0
  31. package/dist/lib/routes/OAuthConsentRoute.js +19 -0
  32. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
  33. package/dist/lib/routes/PricingRoute.js +19 -0
  34. package/dist/lib/routes/PricingRoute.js.map +1 -0
  35. package/dist/lib/routes/PrivacyRoute.js +19 -0
  36. package/dist/lib/routes/PrivacyRoute.js.map +1 -0
  37. package/dist/lib/routes/TeamSettingsRoute.js +19 -0
  38. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
  39. package/dist/lib/routes/TermsRoute.js +19 -0
  40. package/dist/lib/routes/TermsRoute.js.map +1 -0
  41. package/dist/lib/routes/VerifyEmailRoute.js +19 -0
  42. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
  43. package/dist/lib/routes.js +79 -0
  44. package/dist/lib/routes.js.map +1 -0
  45. package/dist/lib/ssr-utils.js +29 -0
  46. package/dist/lib/ssr-utils.js.map +1 -0
  47. package/dist/lib/ssr.js +60 -0
  48. package/dist/lib/ssr.js.map +1 -0
  49. package/dist/lib/styles.css +2410 -0
  50. package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
  51. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
  52. package/dist/logo.svg +12 -0
  53. package/package.json +84 -0
  54. package/src/components/AgentSelector.tsx +90 -0
  55. package/src/components/BranchModal.tsx +129 -0
  56. package/src/components/ClientOnly.tsx +27 -0
  57. package/src/components/ExportMenu.tsx +122 -0
  58. package/src/components/LoadingSkeletons.tsx +110 -0
  59. package/src/components/MCPCredentialsSection.tsx +309 -0
  60. package/src/components/MentionChip.tsx +149 -0
  61. package/src/components/MentionDropdown.tsx +175 -0
  62. package/src/components/MentionInput.tsx +293 -0
  63. package/src/components/MessageItem.tsx +300 -0
  64. package/src/components/MessageList.tsx +159 -0
  65. package/src/components/OAuthAppsSection.tsx +124 -0
  66. package/src/components/ProjectFolder.tsx +141 -0
  67. package/src/components/ProjectModal.tsx +296 -0
  68. package/src/components/SSRMessageList.tsx +153 -0
  69. package/src/components/SearchModal.tsx +173 -0
  70. package/src/components/SettingsModal.tsx +412 -0
  71. package/src/components/ShareModal.tsx +280 -0
  72. package/src/components/Sidebar.tsx +491 -0
  73. package/src/components/TeamSwitcher.tsx +273 -0
  74. package/src/components/ToolCallDisplay.tsx +473 -0
  75. package/src/components/ToolConfirmationModal.tsx +130 -0
  76. package/src/components/UsageChart.tsx +177 -0
  77. package/src/components/content/CodeBlock.tsx +69 -0
  78. package/src/components/content/MarkdownRenderer.tsx +64 -0
  79. package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
  80. package/src/contexts/AuthContext.tsx +119 -0
  81. package/src/contexts/ConfigContext.tsx +214 -0
  82. package/src/contexts/ProjectContext.tsx +167 -0
  83. package/src/contexts/ServerConfigProvider.tsx +41 -0
  84. package/src/contexts/ServerThemeProvider.tsx +47 -0
  85. package/src/contexts/TeamContext.tsx +255 -0
  86. package/src/contexts/ThemeContext.tsx +113 -0
  87. package/src/extensions/index.ts +15 -0
  88. package/src/extensions/registry.ts +187 -0
  89. package/src/extensions/useExtensions.ts +52 -0
  90. package/src/hooks/useAppPath.ts +34 -0
  91. package/src/hooks/useBasePath.ts +13 -0
  92. package/src/hooks/useKeyboardShortcuts.ts +50 -0
  93. package/src/hooks/useMentionSearch.ts +106 -0
  94. package/src/index.tsx +116 -0
  95. package/src/layouts/MainLayout.tsx +98 -0
  96. package/src/pages/AcceptInvitePage.tsx +175 -0
  97. package/src/pages/AdminDashboardPage.tsx +362 -0
  98. package/src/pages/AdminTeamPage.tsx +304 -0
  99. package/src/pages/AdminTeamsPage.tsx +242 -0
  100. package/src/pages/AdminUsersPage.tsx +385 -0
  101. package/src/pages/ApiKeysPage.tsx +449 -0
  102. package/src/pages/ChatPage.tsx +310 -0
  103. package/src/pages/DocumentsPage.tsx +577 -0
  104. package/src/pages/LoginPage.tsx +232 -0
  105. package/src/pages/OAuthConsentPage.tsx +234 -0
  106. package/src/pages/PricingPage.tsx +314 -0
  107. package/src/pages/PrivacyPage.tsx +65 -0
  108. package/src/pages/RegisterPage.tsx +153 -0
  109. package/src/pages/ScheduledPromptsPage.tsx +702 -0
  110. package/src/pages/SharedThreadPage.tsx +116 -0
  111. package/src/pages/TeamSettingsPage.tsx +1085 -0
  112. package/src/pages/TermsPage.tsx +82 -0
  113. package/src/pages/VerifyEmailPage.tsx +202 -0
  114. package/src/routes/AcceptInviteRoute.tsx +24 -0
  115. package/src/routes/AdminDashboardRoute.tsx +24 -0
  116. package/src/routes/AdminTeamRoute.tsx +24 -0
  117. package/src/routes/AdminTeamsRoute.tsx +24 -0
  118. package/src/routes/AdminUsersRoute.tsx +24 -0
  119. package/src/routes/ApiKeysRoute.tsx +24 -0
  120. package/src/routes/AutomationsRoute.tsx +24 -0
  121. package/src/routes/ChatRoute.tsx +28 -0
  122. package/src/routes/DocumentsRoute.tsx +24 -0
  123. package/src/routes/OAuthConsentRoute.tsx +24 -0
  124. package/src/routes/PricingRoute.tsx +24 -0
  125. package/src/routes/PrivacyRoute.tsx +24 -0
  126. package/src/routes/TeamSettingsRoute.tsx +24 -0
  127. package/src/routes/TermsRoute.tsx +24 -0
  128. package/src/routes/VerifyEmailRoute.tsx +24 -0
  129. package/src/routes/index.ts +57 -0
  130. package/src/ssr-utils.tsx +84 -0
  131. package/src/ssr.ts +123 -0
  132. package/src/stores/chatStore.ts +670 -0
  133. package/src/styles/index.css +254 -0
  134. package/src/utils/api.ts +78 -0
  135. package/src/vite-env.d.ts +13 -0
@@ -0,0 +1,141 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router';
3
+ import { ChevronDown, ChevronRight, Folder, MessageSquare, Settings, Plus, Trash2, GitBranch } from 'lucide-react';
4
+ import type { ProjectWithThreadCount, ThreadSummary } from '@chaaskit/shared';
5
+ import { useAppPath } from '../hooks/useAppPath';
6
+
7
+ interface ProjectFolderProps {
8
+ project: ProjectWithThreadCount;
9
+ threads: ThreadSummary[];
10
+ selectedThreadId?: string;
11
+ onEditProject: () => void;
12
+ onNewThread: () => void;
13
+ onSelectThread: (threadId: string) => void;
14
+ onDeleteThread: (threadId: string, e: React.MouseEvent) => void;
15
+ }
16
+
17
+ export default function ProjectFolder({
18
+ project,
19
+ threads,
20
+ selectedThreadId,
21
+ onEditProject,
22
+ onNewThread,
23
+ onSelectThread,
24
+ onDeleteThread,
25
+ }: ProjectFolderProps) {
26
+ const [isExpanded, setIsExpanded] = useState(true);
27
+ const navigate = useNavigate();
28
+ const appPath = useAppPath();
29
+
30
+ const projectThreads = threads.filter((t) => t.projectId === project.id);
31
+
32
+ function handleToggle() {
33
+ setIsExpanded(!isExpanded);
34
+ }
35
+
36
+ function handleEditClick(e: React.MouseEvent) {
37
+ e.stopPropagation();
38
+ onEditProject();
39
+ }
40
+
41
+ function handleNewThreadClick(e: React.MouseEvent) {
42
+ e.stopPropagation();
43
+ onNewThread();
44
+ }
45
+
46
+ function handleThreadClick(threadId: string) {
47
+ onSelectThread(threadId);
48
+ navigate(appPath(`/thread/${threadId}`));
49
+ }
50
+
51
+ return (
52
+ <div className="mb-1">
53
+ {/* Project Header */}
54
+ <div
55
+ onClick={handleToggle}
56
+ className="group flex items-center gap-2 rounded-lg px-2 py-1.5 cursor-pointer transition-colors text-text-secondary hover:bg-background-secondary hover:text-text-primary"
57
+ >
58
+ {/* Expand/collapse chevron */}
59
+ <span className="p-0.5">
60
+ {isExpanded ? (
61
+ <ChevronDown size={14} />
62
+ ) : (
63
+ <ChevronRight size={14} />
64
+ )}
65
+ </span>
66
+
67
+ {/* Folder icon with project color */}
68
+ <Folder
69
+ size={16}
70
+ className="flex-shrink-0"
71
+ style={{ color: project.color }}
72
+ fill={project.color}
73
+ />
74
+
75
+ {/* Project name */}
76
+ <span className="flex-1 text-sm truncate">{project.name}</span>
77
+
78
+ {/* Thread count badge */}
79
+ <span className="text-xs text-text-muted px-1.5 py-0.5 bg-background-secondary rounded">
80
+ {project.threadCount}
81
+ </span>
82
+
83
+ {/* Action buttons (visible on hover) */}
84
+ <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
85
+ <button
86
+ onClick={handleNewThreadClick}
87
+ className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"
88
+ title="New thread in project"
89
+ >
90
+ <Plus size={14} />
91
+ </button>
92
+ <button
93
+ onClick={handleEditClick}
94
+ className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"
95
+ title="Edit project"
96
+ >
97
+ <Settings size={14} />
98
+ </button>
99
+ </div>
100
+ </div>
101
+
102
+ {/* Project Threads */}
103
+ {isExpanded && (
104
+ <div className="ml-6 pl-2 border-l border-border">
105
+ {projectThreads.length === 0 ? (
106
+ <div className="py-2 px-2 text-xs text-text-muted italic">
107
+ No threads yet
108
+ </div>
109
+ ) : (
110
+ projectThreads.map((thread) => (
111
+ <div
112
+ key={thread.id}
113
+ onClick={() => handleThreadClick(thread.id)}
114
+ className={`group flex items-center gap-2 rounded-lg px-2 py-1.5 cursor-pointer transition-colors ${
115
+ selectedThreadId === thread.id
116
+ ? 'bg-background-secondary text-text-primary'
117
+ : 'text-text-secondary hover:bg-background-secondary hover:text-text-primary'
118
+ }`}
119
+ >
120
+ {thread.parentThreadId ? (
121
+ <span title="Branched conversation">
122
+ <GitBranch size={14} className="flex-shrink-0" />
123
+ </span>
124
+ ) : (
125
+ <MessageSquare size={14} className="flex-shrink-0" />
126
+ )}
127
+ <span className="flex-1 text-xs truncate">{thread.title}</span>
128
+ <button
129
+ onClick={(e) => onDeleteThread(thread.id, e)}
130
+ className="rounded p-1 text-text-muted opacity-0 hover:bg-error/10 hover:text-error group-hover:opacity-100 touch-device:opacity-100 transition-opacity"
131
+ >
132
+ <Trash2 size={12} />
133
+ </button>
134
+ </div>
135
+ ))
136
+ )}
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,296 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { X, FolderPlus, Loader2, Trash2 } from 'lucide-react';
4
+ import type { Project, ProjectSharing } from '@chaaskit/shared';
5
+ import { useProject } from '../contexts/ProjectContext';
6
+ import { useTeam } from '../contexts/TeamContext';
7
+
8
+ interface ProjectModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ project?: Project | null; // If provided, we're editing; otherwise creating
12
+ }
13
+
14
+ export default function ProjectModal({ isOpen, onClose, project }: ProjectModalProps) {
15
+ const { createProject, updateProject, archiveProject, projectColors } = useProject();
16
+ const { currentTeamId, getCurrentTeamRole } = useTeam();
17
+ const [name, setName] = useState('');
18
+ const [color, setColor] = useState(projectColors[0] || '#3b82f6');
19
+ const [context, setContext] = useState('');
20
+ const [sharing, setSharing] = useState<ProjectSharing>('private');
21
+ const [isLoading, setIsLoading] = useState(false);
22
+ const [showArchiveConfirm, setShowArchiveConfirm] = useState(false);
23
+
24
+ const isEditing = !!project;
25
+ const teamRole = getCurrentTeamRole();
26
+ const canEdit = !project || project.userId === undefined ||
27
+ (currentTeamId && (teamRole === 'owner' || teamRole === 'admin'));
28
+
29
+ // Reset form when modal opens/closes or project changes
30
+ useEffect(() => {
31
+ if (isOpen) {
32
+ if (project) {
33
+ setName(project.name);
34
+ setColor(project.color);
35
+ setContext(project.context || '');
36
+ setSharing(project.sharing);
37
+ } else {
38
+ setName('');
39
+ setColor(projectColors[0] || '#3b82f6');
40
+ setContext('');
41
+ setSharing('private');
42
+ }
43
+ setShowArchiveConfirm(false);
44
+ }
45
+ }, [isOpen, project, projectColors]);
46
+
47
+ if (!isOpen) return null;
48
+
49
+ async function handleSubmit(e: React.FormEvent) {
50
+ e.preventDefault();
51
+ if (!name.trim()) return;
52
+
53
+ setIsLoading(true);
54
+ try {
55
+ if (isEditing && project) {
56
+ await updateProject(project.id, {
57
+ name: name.trim(),
58
+ color,
59
+ context: context.trim() || null,
60
+ sharing: currentTeamId ? sharing : 'private',
61
+ });
62
+ } else {
63
+ await createProject({
64
+ name: name.trim(),
65
+ color,
66
+ context: context.trim() || undefined,
67
+ sharing: currentTeamId ? sharing : 'private',
68
+ });
69
+ }
70
+ onClose();
71
+ } catch (error) {
72
+ console.error('Failed to save project:', error);
73
+ } finally {
74
+ setIsLoading(false);
75
+ }
76
+ }
77
+
78
+ async function handleArchive() {
79
+ if (!project) return;
80
+
81
+ setIsLoading(true);
82
+ try {
83
+ await archiveProject(project.id);
84
+ onClose();
85
+ } catch (error) {
86
+ console.error('Failed to archive project:', error);
87
+ } finally {
88
+ setIsLoading(false);
89
+ }
90
+ }
91
+
92
+ return createPortal(
93
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
94
+ <div className="w-full max-w-lg rounded-lg bg-background border border-border shadow-xl">
95
+ {/* Header */}
96
+ <div className="flex items-center justify-between border-b border-border p-4">
97
+ <div className="flex items-center gap-2">
98
+ <FolderPlus size={20} className="text-primary" />
99
+ <h2 className="text-lg font-semibold text-text-primary">
100
+ {isEditing ? 'Edit Project' : 'Create Project'}
101
+ </h2>
102
+ </div>
103
+ <button
104
+ onClick={onClose}
105
+ className="rounded p-1 text-text-muted hover:bg-background-secondary hover:text-text-primary"
106
+ >
107
+ <X size={20} />
108
+ </button>
109
+ </div>
110
+
111
+ {/* Content */}
112
+ <form onSubmit={handleSubmit} className="p-4 space-y-4">
113
+ {/* Name */}
114
+ <div>
115
+ <label className="block text-sm font-medium text-text-secondary mb-2">
116
+ Project Name
117
+ </label>
118
+ <input
119
+ type="text"
120
+ value={name}
121
+ onChange={(e) => setName(e.target.value)}
122
+ placeholder="My Project"
123
+ className="w-full rounded-lg border border-border bg-background p-3 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
124
+ autoFocus
125
+ required
126
+ maxLength={100}
127
+ disabled={!canEdit}
128
+ />
129
+ </div>
130
+
131
+ {/* Color Picker */}
132
+ <div>
133
+ <label className="block text-sm font-medium text-text-secondary mb-2">
134
+ Color
135
+ </label>
136
+ <div className="flex gap-2 flex-wrap">
137
+ {projectColors.map((c) => (
138
+ <button
139
+ key={c}
140
+ type="button"
141
+ onClick={() => canEdit && setColor(c)}
142
+ className={`w-8 h-8 rounded-full border-2 transition-all ${
143
+ color === c
144
+ ? 'border-text-primary scale-110'
145
+ : 'border-transparent hover:scale-105'
146
+ } ${!canEdit ? 'cursor-not-allowed opacity-50' : ''}`}
147
+ style={{ backgroundColor: c }}
148
+ disabled={!canEdit}
149
+ />
150
+ ))}
151
+ </div>
152
+ </div>
153
+
154
+ {/* Context */}
155
+ <div>
156
+ <label className="block text-sm font-medium text-text-secondary mb-2">
157
+ Project Context (Optional)
158
+ </label>
159
+ <textarea
160
+ value={context}
161
+ onChange={(e) => setContext(e.target.value)}
162
+ placeholder="Add context that the AI will use for all conversations in this project..."
163
+ className="w-full rounded-lg border border-border bg-background p-3 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none resize-none"
164
+ rows={4}
165
+ maxLength={10000}
166
+ disabled={!canEdit}
167
+ />
168
+ <p className="mt-1 text-xs text-text-muted">
169
+ This context is included in all AI conversations within this project.
170
+ </p>
171
+ </div>
172
+
173
+ {/* Sharing (only show for team projects) */}
174
+ {currentTeamId && (
175
+ <div>
176
+ <label className="block text-sm font-medium text-text-secondary mb-2">
177
+ Sharing
178
+ </label>
179
+ <div className="space-y-2">
180
+ <label className="flex items-start gap-3 cursor-pointer">
181
+ <input
182
+ type="radio"
183
+ name="sharing"
184
+ value="private"
185
+ checked={sharing === 'private'}
186
+ onChange={() => setSharing('private')}
187
+ className="mt-1"
188
+ disabled={!canEdit}
189
+ />
190
+ <div>
191
+ <div className="text-sm font-medium text-text-primary">Private</div>
192
+ <div className="text-xs text-text-muted">
193
+ Only you can see this project and its threads
194
+ </div>
195
+ </div>
196
+ </label>
197
+ <label className="flex items-start gap-3 cursor-pointer">
198
+ <input
199
+ type="radio"
200
+ name="sharing"
201
+ value="team"
202
+ checked={sharing === 'team'}
203
+ onChange={() => setSharing('team')}
204
+ className="mt-1"
205
+ disabled={!canEdit}
206
+ />
207
+ <div>
208
+ <div className="text-sm font-medium text-text-primary">Team</div>
209
+ <div className="text-xs text-text-muted">
210
+ All team members can see and contribute to this project
211
+ </div>
212
+ </div>
213
+ </label>
214
+ </div>
215
+ </div>
216
+ )}
217
+
218
+ {/* Archive section (only for editing) */}
219
+ {isEditing && canEdit && (
220
+ <div className="pt-4 border-t border-border">
221
+ {!showArchiveConfirm ? (
222
+ <button
223
+ type="button"
224
+ onClick={() => setShowArchiveConfirm(true)}
225
+ className="flex items-center gap-2 text-sm text-error hover:text-error/80"
226
+ >
227
+ <Trash2 size={16} />
228
+ Archive Project
229
+ </button>
230
+ ) : (
231
+ <div className="p-3 bg-error/10 rounded-lg border border-error/20">
232
+ <p className="text-sm text-text-primary mb-3">
233
+ Are you sure you want to archive this project? All threads in this project will also be archived.
234
+ </p>
235
+ <div className="flex gap-2">
236
+ <button
237
+ type="button"
238
+ onClick={handleArchive}
239
+ disabled={isLoading}
240
+ className="flex items-center gap-2 rounded-lg bg-error px-3 py-1.5 text-sm font-medium text-white hover:bg-error/80 disabled:opacity-50"
241
+ >
242
+ {isLoading ? (
243
+ <Loader2 size={14} className="animate-spin" />
244
+ ) : (
245
+ <Trash2 size={14} />
246
+ )}
247
+ Archive
248
+ </button>
249
+ <button
250
+ type="button"
251
+ onClick={() => setShowArchiveConfirm(false)}
252
+ className="rounded-lg px-3 py-1.5 text-sm font-medium text-text-secondary hover:bg-background-secondary"
253
+ >
254
+ Cancel
255
+ </button>
256
+ </div>
257
+ </div>
258
+ )}
259
+ </div>
260
+ )}
261
+
262
+ {/* Footer */}
263
+ <div className="flex justify-end gap-2 pt-4">
264
+ <button
265
+ type="button"
266
+ onClick={onClose}
267
+ className="rounded-lg px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary"
268
+ >
269
+ Cancel
270
+ </button>
271
+ {canEdit && (
272
+ <button
273
+ type="submit"
274
+ disabled={isLoading || !name.trim()}
275
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
276
+ >
277
+ {isLoading ? (
278
+ <>
279
+ <Loader2 size={16} className="animate-spin" />
280
+ {isEditing ? 'Saving...' : 'Creating...'}
281
+ </>
282
+ ) : (
283
+ <>
284
+ <FolderPlus size={16} />
285
+ {isEditing ? 'Save Changes' : 'Create Project'}
286
+ </>
287
+ )}
288
+ </button>
289
+ )}
290
+ </div>
291
+ </form>
292
+ </div>
293
+ </div>,
294
+ document.body
295
+ );
296
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Server-safe message list component for SSR.
3
+ * This is a simplified, read-only version of MessageList that doesn't use:
4
+ * - Browser APIs (localStorage, window, navigator)
5
+ * - React hooks that require client-side state (useState, useEffect)
6
+ * - Context providers that require browser APIs (ThemeContext, etc.)
7
+ *
8
+ * For interactive features, hydrate with the full MessageList component.
9
+ */
10
+
11
+ import type { Message } from '@chaaskit/shared';
12
+ import { SSRMarkdownRenderer } from './content/SSRMarkdownRenderer';
13
+
14
+ interface SSRMessageListProps {
15
+ messages: Message[];
16
+ appName?: string;
17
+ }
18
+
19
+ function UserIcon() {
20
+ return (
21
+ <svg
22
+ xmlns="http://www.w3.org/2000/svg"
23
+ width="12"
24
+ height="12"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ strokeWidth="2"
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ >
32
+ <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
33
+ <circle cx="12" cy="7" r="4" />
34
+ </svg>
35
+ );
36
+ }
37
+
38
+ function BotIcon() {
39
+ return (
40
+ <svg
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ width="12"
43
+ height="12"
44
+ viewBox="0 0 24 24"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ strokeWidth="2"
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ >
51
+ <path d="M12 8V4H8" />
52
+ <rect width="16" height="12" x="4" y="8" rx="2" />
53
+ <path d="M2 14h2" />
54
+ <path d="M20 14h2" />
55
+ <path d="M15 13v2" />
56
+ <path d="M9 13v2" />
57
+ </svg>
58
+ );
59
+ }
60
+
61
+ function SSRMessageItem({ message }: { message: Message }) {
62
+ const isUser = message.role === 'user';
63
+
64
+ if (isUser) {
65
+ return (
66
+ <div className="flex gap-3 flex-row-reverse">
67
+ {/* Avatar */}
68
+ <div
69
+ className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary"
70
+ style={{ backgroundColor: 'rgb(var(--color-primary))' }}
71
+ >
72
+ <span className="text-white">
73
+ <UserIcon />
74
+ </span>
75
+ </div>
76
+
77
+ {/* Message Content */}
78
+ <div
79
+ className="flex max-w-[85%] flex-col items-end"
80
+ style={{ maxWidth: '85%' }}
81
+ >
82
+ <div
83
+ className="rounded-lg px-3 py-2 bg-user-message-bg text-user-message-text"
84
+ style={{
85
+ backgroundColor: 'rgb(var(--color-user-message-bg))',
86
+ color: 'rgb(var(--color-user-message-text))',
87
+ borderRadius: '0.5rem',
88
+ padding: '0.5rem 0.75rem',
89
+ }}
90
+ >
91
+ <p style={{ whiteSpace: 'pre-wrap', fontSize: '0.875rem', margin: 0 }}>
92
+ {message.content}
93
+ </p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ // Assistant message
101
+ return (
102
+ <div className="flex gap-3">
103
+ {/* Avatar */}
104
+ <div
105
+ className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-secondary"
106
+ style={{ backgroundColor: 'rgb(var(--color-secondary))' }}
107
+ >
108
+ <span className="text-white">
109
+ <BotIcon />
110
+ </span>
111
+ </div>
112
+
113
+ {/* Message Content */}
114
+ <div className="flex max-w-[85%] flex-col items-start" style={{ maxWidth: '85%' }}>
115
+ <div
116
+ className="rounded-lg px-3 py-2 bg-assistant-message-bg text-assistant-message-text"
117
+ style={{
118
+ backgroundColor: 'rgb(var(--color-assistant-message-bg))',
119
+ color: 'rgb(var(--color-assistant-message-text))',
120
+ borderRadius: '0.5rem',
121
+ padding: '0.5rem 0.75rem',
122
+ }}
123
+ >
124
+ <div className="markdown-content" style={{ fontSize: '0.875rem' }}>
125
+ <SSRMarkdownRenderer content={message.content} />
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ export function SSRMessageList({ messages }: SSRMessageListProps) {
134
+ return (
135
+ <div
136
+ className="mx-auto max-w-3xl px-4 py-6"
137
+ style={{
138
+ maxWidth: '48rem',
139
+ marginLeft: 'auto',
140
+ marginRight: 'auto',
141
+ padding: '1.5rem 1rem',
142
+ }}
143
+ >
144
+ <div className="space-y-4" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
145
+ {messages.map((message) => (
146
+ <SSRMessageItem key={message.id} message={message} />
147
+ ))}
148
+ </div>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ export default SSRMessageList;