@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.
- package/dist/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- 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
|
+
}
|