forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdemo-source.tsx
More file actions
175 lines (158 loc) · 5.31 KB
/
Copy pathdemo-source.tsx
File metadata and controls
175 lines (158 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// <DemoSource> — in-page source viewer used inside <InlineDemo>'s Code
// tab. Reads from the same `demo-content.json` bundle <Snippet> consumes,
// scoped to one (integration, demo) cell.
//
// Defaults to showing only files flagged in the manifest's `highlight:`
// array (rendered in manifest order via `highlightOrder`). Falls back to
// the full bundled file list when nothing is flagged. Pass
// `onlyHighlighted={false}` to always show every file in the cell.
//
// Rendering: Fumadocs's `<CodeBlockTabs>` for the per-file tab strip and
// `<DynamicCodeBlock>` (Shiki at runtime via `useShiki`) for the body of
// each tab. This shares chrome and syntax-highlight theme with the
// authored fenced blocks rendered through MDX, so the Code tab inside
// InlineDemo looks identical to the rest of the page.
"use client";
import React, { useMemo } from "react";
import {
CodeBlockTab,
CodeBlockTabs,
CodeBlockTabsList,
CodeBlockTabsTrigger,
} from "fumadocs-ui/components/codeblock";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import demoContent from "../data/demo-content.json";
interface DemoFile {
filename: string;
language: string;
content: string;
highlighted?: boolean;
highlightOrder?: number;
}
interface DemoRecord {
files?: DemoFile[];
}
const demos: Record<string, DemoRecord> = (
demoContent as { demos: Record<string, DemoRecord> }
).demos;
// Manifest language slugs → Shiki language names. Shiki recognises most
// slugs directly; this map exists only to disambiguate aliases authors
// write differently from Shiki's canonical names.
const SHIKI_LANGUAGE_MAP: Record<string, string> = {
typescript: "typescript",
javascript: "javascript",
python: "python",
csharp: "csharp",
css: "css",
json: "json",
yaml: "yaml",
markdown: "markdown",
text: "plaintext",
};
function resolveShikiLanguage(lang: string): string {
return SHIKI_LANGUAGE_MAP[lang] ?? lang;
}
function basename(p: string): string {
const idx = p.lastIndexOf("/");
return idx === -1 ? p : p.slice(idx + 1);
}
function WarningBox({ children }: { children: React.ReactNode }) {
return (
<div
className="shell-docs-radius-surface shell-docs-warning-surface my-4 border border-l-4 p-4 text-sm text-[var(--text-secondary)] shadow-[var(--shadow-control)]"
role="alert"
>
<div className="font-semibold mb-1 text-[var(--text)]">
Missing demo source
</div>
{children}
</div>
);
}
interface DemoSourceProps {
/** Integration slug, e.g. `langgraph-python`. Used directly as the
* bundle key prefix — no `getDocsFolder` translation. */
integration: string;
/** Demo id, e.g. `agentic-chat`. Matches manifest `demos[i].id`. */
demo: string;
/**
* When true (default), only files flagged `highlighted: true` in the
* manifest are shown, sorted by `highlightOrder`. If no files are
* flagged, falls back to showing all files so the Code tab is never
* blank. Pass `false` to always show every file in the cell.
*/
onlyHighlighted?: boolean;
}
/**
* Escape a filename to the radix-tabs `value` format Fumadocs's
* `<Tabs items={[...]}>` derives internally — lowercase + first
* whitespace replaced with `-`. Keeps `CodeBlockTabsTrigger` /
* `CodeBlockTab` value props in sync for radix pairing.
*/
function tabValue(filename: string): string {
return filename.toLowerCase().replace(/\s/, "-");
}
export function DemoSource({
integration,
demo,
onlyHighlighted = true,
}: DemoSourceProps) {
const key = `${integration}::${demo}`;
const record = demos[key];
const displayFiles: DemoFile[] = useMemo(() => {
const all = record?.files ?? [];
if (!onlyHighlighted) return all;
const flagged = all.filter((f) => f.highlighted);
if (flagged.length === 0) return all;
return [...flagged].sort((a, b) => {
const ao = a.highlightOrder;
const bo = b.highlightOrder;
if (ao === undefined && bo === undefined)
return a.filename.localeCompare(b.filename);
if (ao === undefined) return 1;
if (bo === undefined) return -1;
return ao - bo;
});
}, [record, onlyHighlighted]);
if (!record) {
return (
<WarningBox>
No demo found for <code>{key}</code>. Check the integration slug and
demo id (matches manifest <code>slug</code> / <code>demos[i].id</code>
).
</WarningBox>
);
}
if (displayFiles.length === 0) {
return (
<WarningBox>
Demo <code>{key}</code> has no bundled source files. The manifest may be
missing a <code>route:</code> or its demo folder is empty.
</WarningBox>
);
}
const defaultValue = tabValue(displayFiles[0].filename);
return (
<CodeBlockTabs defaultValue={defaultValue}>
<CodeBlockTabsList>
{displayFiles.map((f) => (
<CodeBlockTabsTrigger
key={f.filename}
value={tabValue(f.filename)}
title={f.filename}
>
{basename(f.filename)}
</CodeBlockTabsTrigger>
))}
</CodeBlockTabsList>
{displayFiles.map((f) => (
<CodeBlockTab key={f.filename} value={tabValue(f.filename)}>
<DynamicCodeBlock
lang={resolveShikiLanguage(f.language)}
code={f.content.replace(/\n$/, "")}
/>
</CodeBlockTab>
))}
</CodeBlockTabs>
);
}