@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,177 @@
1
+ import { useState, useMemo } from 'react';
2
+ import type { UsageDataPoint } from '@chaaskit/shared';
3
+
4
+ type MetricType = 'messages' | 'inputTokens' | 'outputTokens' | 'totalTokens';
5
+
6
+ interface UsageChartProps {
7
+ data: UsageDataPoint[];
8
+ metric: MetricType;
9
+ isLoading?: boolean;
10
+ }
11
+
12
+ export default function UsageChart({ data, metric, isLoading }: UsageChartProps) {
13
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
14
+
15
+ const { values, maxValue, total, average, peakDay } = useMemo(() => {
16
+ const values = data.map((d) => {
17
+ if (metric === 'totalTokens') {
18
+ return d.inputTokens + d.outputTokens;
19
+ }
20
+ return d[metric];
21
+ });
22
+
23
+ const maxValue = Math.max(...values, 1);
24
+ const total = values.reduce((a, b) => a + b, 0);
25
+ const average = values.length > 0 ? total / values.length : 0;
26
+
27
+ let peakIndex = 0;
28
+ let peakValue = 0;
29
+ values.forEach((v, i) => {
30
+ if (v > peakValue) {
31
+ peakValue = v;
32
+ peakIndex = i;
33
+ }
34
+ });
35
+ const peakDay = data[peakIndex]?.date || '';
36
+
37
+ return { values, maxValue, total, average, peakDay };
38
+ }, [data, metric]);
39
+
40
+ const metricLabels: Record<MetricType, string> = {
41
+ messages: 'messages',
42
+ inputTokens: 'input tokens',
43
+ outputTokens: 'output tokens',
44
+ totalTokens: 'total tokens',
45
+ };
46
+
47
+ const formatValue = (value: number): string => {
48
+ if (value >= 1_000_000) {
49
+ return `${(value / 1_000_000).toFixed(1)}M`;
50
+ }
51
+ if (value >= 1_000) {
52
+ return `${(value / 1_000).toFixed(1)}K`;
53
+ }
54
+ return value.toLocaleString();
55
+ };
56
+
57
+ const formatDate = (dateStr: string): string => {
58
+ const date = new Date(dateStr);
59
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
60
+ };
61
+
62
+ if (isLoading) {
63
+ return (
64
+ <div className="h-64 flex items-center justify-center">
65
+ <div className="animate-pulse flex flex-col items-center gap-2">
66
+ <div className="h-32 w-full flex items-end justify-center gap-1">
67
+ {Array.from({ length: 15 }).map((_, i) => (
68
+ <div
69
+ key={i}
70
+ className="w-4 bg-[var(--color-background-secondary)] rounded-t"
71
+ style={{ height: `${Math.random() * 80 + 20}%` }}
72
+ />
73
+ ))}
74
+ </div>
75
+ <div className="h-4 w-48 bg-[var(--color-background-secondary)] rounded" />
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (data.length === 0) {
82
+ return (
83
+ <div className="h-64 flex items-center justify-center text-[var(--color-text-muted)]">
84
+ No usage data available for this period
85
+ </div>
86
+ );
87
+ }
88
+
89
+ // Determine label frequency based on data length
90
+ const labelFrequency = data.length > 60 ? 14 : data.length > 30 ? 7 : data.length > 14 ? 3 : 1;
91
+
92
+ return (
93
+ <div className="space-y-4">
94
+ {/* Chart */}
95
+ <div className="relative h-48">
96
+ <div className="absolute inset-0 flex items-end gap-[2px]">
97
+ {values.map((value, index) => {
98
+ const height = maxValue > 0 ? (value / maxValue) * 100 : 0;
99
+ const isHovered = hoveredIndex === index;
100
+
101
+ return (
102
+ <div
103
+ key={index}
104
+ className="flex-1 relative group"
105
+ onMouseEnter={() => setHoveredIndex(index)}
106
+ onMouseLeave={() => setHoveredIndex(null)}
107
+ >
108
+ <div
109
+ className={`w-full rounded-t transition-all cursor-pointer ${
110
+ isHovered
111
+ ? 'bg-[var(--color-primary)]'
112
+ : 'bg-[var(--color-primary)]/60 hover:bg-[var(--color-primary)]/80'
113
+ }`}
114
+ style={{ height: `${Math.max(height, 2)}%` }}
115
+ />
116
+
117
+ {/* Tooltip */}
118
+ {isHovered && (
119
+ <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-10 whitespace-nowrap">
120
+ <div className="bg-[var(--color-background)] border border-[var(--color-border)] rounded-lg shadow-lg px-3 py-2">
121
+ <p className="text-sm font-medium text-[var(--color-text-primary)]">
122
+ {formatDate(data[index].date)}
123
+ </p>
124
+ <p className="text-sm text-[var(--color-text-muted)]">
125
+ {value.toLocaleString()} {metricLabels[metric]}
126
+ </p>
127
+ </div>
128
+ <div className="absolute left-1/2 -translate-x-1/2 -bottom-1 w-2 h-2 bg-[var(--color-background)] border-r border-b border-[var(--color-border)] rotate-45" />
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ })}
134
+ </div>
135
+ </div>
136
+
137
+ {/* X-axis labels */}
138
+ <div className="flex justify-between text-xs text-[var(--color-text-muted)] px-1">
139
+ {data.map((d, i) => {
140
+ if (i % labelFrequency !== 0 && i !== data.length - 1) {
141
+ return <span key={i} className="flex-1" />;
142
+ }
143
+ return (
144
+ <span key={i} className="flex-1 text-center">
145
+ {formatDate(d.date)}
146
+ </span>
147
+ );
148
+ })}
149
+ </div>
150
+
151
+ {/* Summary row */}
152
+ <div className="flex items-center justify-center gap-6 pt-3 mt-1 border-t border-[var(--color-background)] text-sm">
153
+ <div className="text-center">
154
+ <span className="text-[var(--color-text-muted)]">Total: </span>
155
+ <span className="font-medium text-[var(--color-text-primary)]">
156
+ {formatValue(total)} {metricLabels[metric]}
157
+ </span>
158
+ </div>
159
+ <div className="text-center">
160
+ <span className="text-[var(--color-text-muted)]">Avg: </span>
161
+ <span className="font-medium text-[var(--color-text-primary)]">
162
+ {formatValue(Math.round(average))}/day
163
+ </span>
164
+ </div>
165
+ <div className="text-center">
166
+ <span className="text-[var(--color-text-muted)]">Peak: </span>
167
+ <span className="font-medium text-[var(--color-text-primary)]">
168
+ {formatValue(Math.max(...values))}
169
+ </span>
170
+ {peakDay && (
171
+ <span className="text-[var(--color-text-muted)]"> ({formatDate(peakDay)})</span>
172
+ )}
173
+ </div>
174
+ </div>
175
+ </div>
176
+ );
177
+ }
@@ -0,0 +1,69 @@
1
+ import { useState } from 'react';
2
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ import { Copy, Check } from 'lucide-react';
5
+ import { useTheme } from '../../contexts/ThemeContext';
6
+
7
+ interface CodeBlockProps {
8
+ language: string;
9
+ code: string;
10
+ }
11
+
12
+ export default function CodeBlock({ language, code }: CodeBlockProps) {
13
+ const [copied, setCopied] = useState(false);
14
+ const { theme } = useTheme();
15
+
16
+ async function handleCopy() {
17
+ await navigator.clipboard.writeText(code);
18
+ setCopied(true);
19
+ setTimeout(() => setCopied(false), 2000);
20
+ }
21
+
22
+ const style = theme === 'dark' ? oneDark : oneLight;
23
+
24
+ return (
25
+ <div className="group relative my-4 overflow-hidden rounded-lg border border-border">
26
+ {/* Header */}
27
+ <div className="flex items-center justify-between bg-background-secondary px-4 py-2">
28
+ <span className="text-xs font-medium text-text-muted uppercase">
29
+ {language}
30
+ </span>
31
+ <button
32
+ onClick={handleCopy}
33
+ className="flex items-center gap-1 rounded px-2 py-1 text-xs text-text-muted hover:bg-background hover:text-text-primary"
34
+ >
35
+ {copied ? (
36
+ <>
37
+ <Check size={12} />
38
+ Copied!
39
+ </>
40
+ ) : (
41
+ <>
42
+ <Copy size={12} />
43
+ Copy
44
+ </>
45
+ )}
46
+ </button>
47
+ </div>
48
+
49
+ {/* Code */}
50
+ <SyntaxHighlighter
51
+ language={language}
52
+ style={style}
53
+ customStyle={{
54
+ margin: 0,
55
+ padding: '1rem',
56
+ background: 'transparent',
57
+ fontSize: '0.875rem',
58
+ }}
59
+ codeTagProps={{
60
+ style: {
61
+ fontFamily: 'var(--font-mono)',
62
+ },
63
+ }}
64
+ >
65
+ {code}
66
+ </SyntaxHighlighter>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,64 @@
1
+ import ReactMarkdown from 'react-markdown';
2
+ import remarkGfm from 'remark-gfm';
3
+ import CodeBlock from './CodeBlock';
4
+
5
+ interface MarkdownRendererProps {
6
+ content: string;
7
+ }
8
+
9
+ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
10
+ return (
11
+ <ReactMarkdown
12
+ remarkPlugins={[remarkGfm]}
13
+ components={{
14
+ code({ node, className, children, ...props }) {
15
+ const match = /language-(\w+)/.exec(className || '');
16
+ const isInline = !match && !String(children).includes('\n');
17
+
18
+ if (isInline) {
19
+ return (
20
+ <code
21
+ className="rounded bg-background-secondary px-1.5 py-0.5 font-mono text-sm"
22
+ {...props}
23
+ >
24
+ {children}
25
+ </code>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <CodeBlock
31
+ language={match?.[1] || 'text'}
32
+ code={String(children).replace(/\n$/, '')}
33
+ />
34
+ );
35
+ },
36
+ pre({ children }) {
37
+ // Avoid double wrapping from ReactMarkdown
38
+ return <>{children}</>;
39
+ },
40
+ a({ href, children }) {
41
+ return (
42
+ <a
43
+ href={href}
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ className="text-primary hover:underline"
47
+ >
48
+ {children}
49
+ </a>
50
+ );
51
+ },
52
+ table({ children }) {
53
+ return (
54
+ <div className="my-4 overflow-x-auto">
55
+ <table className="min-w-full border-collapse">{children}</table>
56
+ </div>
57
+ );
58
+ },
59
+ }}
60
+ >
61
+ {content}
62
+ </ReactMarkdown>
63
+ );
64
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Server-safe markdown renderer for SSR.
3
+ * Uses react-markdown which is SSR-compatible.
4
+ * Does not use browser APIs or client-side state.
5
+ */
6
+
7
+ import ReactMarkdown from 'react-markdown';
8
+ import remarkGfm from 'remark-gfm';
9
+
10
+ interface SSRMarkdownRendererProps {
11
+ content: string;
12
+ }
13
+
14
+ export function SSRMarkdownRenderer({ content }: SSRMarkdownRendererProps) {
15
+ return (
16
+ <ReactMarkdown
17
+ remarkPlugins={[remarkGfm]}
18
+ components={{
19
+ code({ node, className, children, ...props }) {
20
+ const match = /language-(\w+)/.exec(className || '');
21
+ const isInline = !match && !String(children).includes('\n');
22
+
23
+ if (isInline) {
24
+ return (
25
+ <code
26
+ style={{
27
+ backgroundColor: 'rgb(var(--color-background-secondary))',
28
+ padding: '0.125rem 0.375rem',
29
+ borderRadius: '0.25rem',
30
+ fontFamily: 'var(--font-mono)',
31
+ fontSize: '0.875rem',
32
+ }}
33
+ {...props}
34
+ >
35
+ {children}
36
+ </code>
37
+ );
38
+ }
39
+
40
+ // For code blocks, render a simple pre/code without syntax highlighting
41
+ // Syntax highlighting will be added on client-side hydration
42
+ const language = match?.[1] || 'text';
43
+ const codeString = String(children).replace(/\n$/, '');
44
+
45
+ return (
46
+ <div
47
+ style={{
48
+ margin: '1rem 0',
49
+ borderRadius: '0.5rem',
50
+ border: '1px solid rgb(var(--color-border))',
51
+ overflow: 'hidden',
52
+ }}
53
+ >
54
+ {/* Header */}
55
+ <div
56
+ style={{
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ justifyContent: 'space-between',
60
+ backgroundColor: 'rgb(var(--color-background-secondary))',
61
+ padding: '0.5rem 1rem',
62
+ }}
63
+ >
64
+ <span
65
+ style={{
66
+ fontSize: '0.75rem',
67
+ fontWeight: 500,
68
+ color: 'rgb(var(--color-text-muted))',
69
+ textTransform: 'uppercase',
70
+ }}
71
+ >
72
+ {language}
73
+ </span>
74
+ </div>
75
+
76
+ {/* Code */}
77
+ <pre
78
+ style={{
79
+ margin: 0,
80
+ padding: '1rem',
81
+ overflow: 'auto',
82
+ backgroundColor: 'rgb(var(--color-background-secondary))',
83
+ }}
84
+ >
85
+ <code
86
+ style={{
87
+ fontFamily: 'var(--font-mono)',
88
+ fontSize: '0.875rem',
89
+ whiteSpace: 'pre',
90
+ }}
91
+ >
92
+ {codeString}
93
+ </code>
94
+ </pre>
95
+ </div>
96
+ );
97
+ },
98
+ pre({ children }) {
99
+ // Avoid double wrapping from ReactMarkdown
100
+ return <>{children}</>;
101
+ },
102
+ a({ href, children }) {
103
+ return (
104
+ <a
105
+ href={href}
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ style={{ color: 'rgb(var(--color-primary))' }}
109
+ >
110
+ {children}
111
+ </a>
112
+ );
113
+ },
114
+ table({ children }) {
115
+ return (
116
+ <div style={{ margin: '1rem 0', overflowX: 'auto' }}>
117
+ <table style={{ minWidth: '100%', borderCollapse: 'collapse' }}>
118
+ {children}
119
+ </table>
120
+ </div>
121
+ );
122
+ },
123
+ th({ children }) {
124
+ return (
125
+ <th
126
+ style={{
127
+ border: '1px solid rgb(var(--color-border))',
128
+ padding: '0.5rem 1rem',
129
+ textAlign: 'left',
130
+ backgroundColor: 'rgb(var(--color-background-secondary))',
131
+ fontWeight: 600,
132
+ }}
133
+ >
134
+ {children}
135
+ </th>
136
+ );
137
+ },
138
+ td({ children }) {
139
+ return (
140
+ <td
141
+ style={{
142
+ border: '1px solid rgb(var(--color-border))',
143
+ padding: '0.5rem 1rem',
144
+ textAlign: 'left',
145
+ }}
146
+ >
147
+ {children}
148
+ </td>
149
+ );
150
+ },
151
+ }}
152
+ >
153
+ {content}
154
+ </ReactMarkdown>
155
+ );
156
+ }
157
+
158
+ export default SSRMarkdownRenderer;
@@ -0,0 +1,119 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useEffect,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import type { UserSession } from '@chaaskit/shared';
9
+ import { api } from '../utils/api';
10
+
11
+ interface RegisterResult {
12
+ requiresVerification: boolean;
13
+ }
14
+
15
+ interface LoginResult {
16
+ requiresVerification: boolean;
17
+ }
18
+
19
+ interface AuthContextType {
20
+ user: UserSession | null;
21
+ isLoading: boolean;
22
+ login: (email: string, password: string) => Promise<LoginResult>;
23
+ register: (email: string, password: string, name?: string) => Promise<RegisterResult>;
24
+ logout: () => Promise<void>;
25
+ sendMagicLink: (email: string) => Promise<void>;
26
+ verifyEmail: (code: string) => Promise<void>;
27
+ resendVerification: () => Promise<void>;
28
+ }
29
+
30
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
31
+
32
+ export function AuthProvider({ children }: { children: ReactNode }) {
33
+ const [user, setUser] = useState<UserSession | null>(null);
34
+ const [isLoading, setIsLoading] = useState(true);
35
+
36
+ useEffect(() => {
37
+ // Check for existing session
38
+ checkAuth();
39
+ }, []);
40
+
41
+ async function checkAuth() {
42
+ try {
43
+ const response = await api.get<{ user: UserSession }>('/api/auth/me');
44
+ setUser(response.user);
45
+ } catch {
46
+ setUser(null);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ }
51
+
52
+ async function login(email: string, password: string): Promise<LoginResult> {
53
+ const response = await api.post<{
54
+ user: UserSession;
55
+ token: string;
56
+ requiresVerification?: boolean;
57
+ }>('/api/auth/login', { email, password });
58
+ setUser(response.user);
59
+ return { requiresVerification: response.requiresVerification ?? false };
60
+ }
61
+
62
+ async function register(email: string, password: string, name?: string): Promise<RegisterResult> {
63
+ const response = await api.post<{
64
+ user: UserSession;
65
+ token: string;
66
+ requiresVerification?: boolean;
67
+ }>('/api/auth/register', { email, password, name });
68
+ setUser(response.user);
69
+ return { requiresVerification: response.requiresVerification ?? false };
70
+ }
71
+
72
+ async function logout() {
73
+ await api.post('/api/auth/logout', {});
74
+ setUser(null);
75
+ }
76
+
77
+ async function sendMagicLink(email: string) {
78
+ await api.post('/api/auth/magic-link', { email });
79
+ }
80
+
81
+ async function verifyEmail(code: string) {
82
+ await api.post<{ verified: boolean; message: string }>(
83
+ '/api/auth/verify-email',
84
+ { code }
85
+ );
86
+ // Refresh user data after verification
87
+ const response = await api.get<{ user: UserSession }>('/api/auth/me');
88
+ setUser(response.user);
89
+ }
90
+
91
+ async function resendVerification() {
92
+ await api.post<{ message: string }>('/api/auth/resend-verification', {});
93
+ }
94
+
95
+ return (
96
+ <AuthContext.Provider
97
+ value={{
98
+ user,
99
+ isLoading,
100
+ login,
101
+ register,
102
+ logout,
103
+ sendMagicLink,
104
+ verifyEmail,
105
+ resendVerification,
106
+ }}
107
+ >
108
+ {children}
109
+ </AuthContext.Provider>
110
+ );
111
+ }
112
+
113
+ export function useAuth() {
114
+ const context = useContext(AuthContext);
115
+ if (context === undefined) {
116
+ throw new Error('useAuth must be used within an AuthProvider');
117
+ }
118
+ return context;
119
+ }