create-interview-cockpit 0.4.0 → 0.6.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/package.json +1 -1
- package/template/client/package-lock.json +753 -1
- package/template/client/package.json +4 -0
- package/template/client/src/App.tsx +20 -0
- package/template/client/src/api.ts +455 -3
- package/template/client/src/components/AiSettingsModal.tsx +855 -248
- package/template/client/src/components/AnnotationDialog.tsx +3 -9
- package/template/client/src/components/ChatMessage.tsx +132 -27
- package/template/client/src/components/ChatView.tsx +365 -123
- package/template/client/src/components/CodeContextPanel.tsx +714 -0
- package/template/client/src/components/CodeLineAnnotationPopup.tsx +179 -0
- package/template/client/src/components/CodeRunnerModal.tsx +3030 -0
- package/template/client/src/components/DocRefModal.tsx +551 -0
- package/template/client/src/components/FileAttachments.tsx +128 -12
- package/template/client/src/components/FilePickerModal.tsx +181 -0
- package/template/client/src/components/FileViewerModal.tsx +406 -28
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +219 -2
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +397 -127
- package/template/client/src/components/TextAnnotator.tsx +8 -15
- package/template/client/src/components/VizCraftEmbed.tsx +412 -25
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +477 -0
- package/template/client/src/store.ts +416 -2
- package/template/client/src/types.ts +41 -1
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/package.json +1 -1
- package/template/server/src/google-drive.ts +144 -2
- package/template/server/src/index.ts +1890 -188
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +274 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { X, MessageSquare, Check, Loader2 } from "lucide-react";
|
|
3
|
+
import type { Question } from "../types";
|
|
4
|
+
import { useStore } from "../store";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
question: Question;
|
|
8
|
+
topicId: string;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function LinkedConvosPicker({
|
|
13
|
+
question,
|
|
14
|
+
topicId,
|
|
15
|
+
onClose,
|
|
16
|
+
}: Props) {
|
|
17
|
+
const { linkConversation, unlinkConversation } = useStore();
|
|
18
|
+
const [siblings, setSiblings] = useState<Question[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [pending, setPending] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetch(`/api/topics/${topicId}/questions`)
|
|
24
|
+
.then((r) => r.json())
|
|
25
|
+
.then((qs: Question[]) => {
|
|
26
|
+
// Exclude the current question itself
|
|
27
|
+
setSiblings(qs.filter((q2) => q2.id !== question.id));
|
|
28
|
+
})
|
|
29
|
+
.finally(() => setLoading(false));
|
|
30
|
+
}, [topicId, question.id]);
|
|
31
|
+
|
|
32
|
+
const linked = new Set(question.linkedConversationIds ?? []);
|
|
33
|
+
|
|
34
|
+
const toggle = async (targetId: string) => {
|
|
35
|
+
setPending(targetId);
|
|
36
|
+
try {
|
|
37
|
+
if (linked.has(targetId)) {
|
|
38
|
+
await unlinkConversation(question.id, targetId);
|
|
39
|
+
} else {
|
|
40
|
+
await linkConversation(question.id, targetId);
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
setPending(null);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
|
49
|
+
<div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[70vh]">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-slate-700 shrink-0">
|
|
52
|
+
<MessageSquare className="w-4 h-4 text-violet-400 shrink-0" />
|
|
53
|
+
<span className="text-sm font-semibold text-slate-200 flex-1">
|
|
54
|
+
Link conversations as context
|
|
55
|
+
</span>
|
|
56
|
+
<button
|
|
57
|
+
onClick={onClose}
|
|
58
|
+
className="p-1 rounded hover:bg-slate-700 text-slate-500 hover:text-slate-300 transition-colors"
|
|
59
|
+
>
|
|
60
|
+
<X className="w-4 h-4" />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Body */}
|
|
65
|
+
<div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-1">
|
|
66
|
+
{loading && (
|
|
67
|
+
<div className="flex items-center justify-center py-8">
|
|
68
|
+
<Loader2 className="w-5 h-5 text-violet-400 animate-spin" />
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{!loading && siblings.length === 0 && (
|
|
72
|
+
<p className="text-xs text-slate-500 text-center py-8">
|
|
73
|
+
No other conversations in this topic yet.
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
{!loading &&
|
|
77
|
+
siblings.map((sibling) => {
|
|
78
|
+
const isLinked = linked.has(sibling.id);
|
|
79
|
+
const isPending = pending === sibling.id;
|
|
80
|
+
const msgCount = sibling.messages?.length ?? 0;
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
key={sibling.id}
|
|
84
|
+
onClick={() => toggle(sibling.id)}
|
|
85
|
+
disabled={isPending}
|
|
86
|
+
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
|
|
87
|
+
isLinked
|
|
88
|
+
? "bg-violet-500/15 border border-violet-500/30 hover:bg-violet-500/20"
|
|
89
|
+
: "bg-slate-800/50 border border-transparent hover:bg-slate-800"
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
className={`w-4 h-4 rounded border shrink-0 flex items-center justify-center transition-colors ${
|
|
94
|
+
isLinked
|
|
95
|
+
? "bg-violet-500 border-violet-500"
|
|
96
|
+
: "border-slate-600"
|
|
97
|
+
}`}
|
|
98
|
+
>
|
|
99
|
+
{isPending ? (
|
|
100
|
+
<Loader2 className="w-2.5 h-2.5 animate-spin text-white" />
|
|
101
|
+
) : isLinked ? (
|
|
102
|
+
<Check className="w-2.5 h-2.5 text-white" />
|
|
103
|
+
) : null}
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex-1 min-w-0">
|
|
106
|
+
<p className="text-xs font-medium text-slate-200 truncate">
|
|
107
|
+
{sibling.title}
|
|
108
|
+
</p>
|
|
109
|
+
<p className="text-[10px] text-slate-500 mt-0.5">
|
|
110
|
+
{msgCount} message{msgCount !== 1 ? "s" : ""}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</button>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Footer hint */}
|
|
119
|
+
<div className="px-4 py-2.5 border-t border-slate-800 shrink-0">
|
|
120
|
+
<p className="text-[10px] text-slate-500">
|
|
121
|
+
Linked conversations are included as read-only background context
|
|
122
|
+
for the AI.
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import ReactMarkdown from "react-markdown";
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
|
-
import { useMemo, useRef, useEffect } from "react";
|
|
3
|
+
import { useMemo, useRef, useEffect, useState } from "react";
|
|
4
4
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
5
5
|
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
6
6
|
import MermaidDiagram from "./MermaidDiagram";
|
|
7
|
+
import PlotEmbed from "./PlotEmbed";
|
|
7
8
|
import VizCraftEmbed from "./VizCraftEmbed";
|
|
8
9
|
import { useStore } from "../store";
|
|
9
|
-
import { Bookmark } from "lucide-react";
|
|
10
|
+
import { Bookmark, ExternalLink, Play, Save, Check } from "lucide-react";
|
|
10
11
|
|
|
11
12
|
import type { Components } from "react-markdown";
|
|
12
13
|
|
|
@@ -15,6 +16,149 @@ interface Props {
|
|
|
15
16
|
onAnnotationClick?: (annotId: string, rect: DOMRect) => void;
|
|
16
17
|
bookmarkedBlockIndex?: number;
|
|
17
18
|
onBookmarkBlock?: (blockIndex: number) => void;
|
|
19
|
+
onSpecRefined?: (originalSpec: string, newSpec: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Inline code block (AI-written code that can be opened in FileViewerModal) ──
|
|
23
|
+
// Parses an optional `// @ref: some-id` first-line directive so the AI can link
|
|
24
|
+
// back to this block anywhere in its response via [Label](inlineref://some-id).
|
|
25
|
+
function InlineCodeBlock({
|
|
26
|
+
codeString,
|
|
27
|
+
language,
|
|
28
|
+
}: {
|
|
29
|
+
codeString: string;
|
|
30
|
+
language: string;
|
|
31
|
+
}) {
|
|
32
|
+
const registerInlineCode = useStore((s) => s.registerInlineCode);
|
|
33
|
+
const openFileViewer = useStore((s) => s.openFileViewer);
|
|
34
|
+
const openCodeRunner = useStore((s) => s.openCodeRunner);
|
|
35
|
+
const currentQuestion = useStore((s) => s.currentQuestion);
|
|
36
|
+
const saveCodeSnippetToQuestion = useStore(
|
|
37
|
+
(s) => s.saveCodeSnippetToQuestion,
|
|
38
|
+
);
|
|
39
|
+
const [savedAi, setSavedAi] = useState<boolean>(false);
|
|
40
|
+
|
|
41
|
+
// Parse optional @ref directive on first line (strips it from display)
|
|
42
|
+
const parsed = useMemo(() => {
|
|
43
|
+
const firstLine = codeString.split("\n")[0]?.trim() ?? "";
|
|
44
|
+
const refMatch = firstLine.match(/^(?:\/\/|#|--)\s*@ref:\s*(.+)$/);
|
|
45
|
+
if (refMatch) {
|
|
46
|
+
const label = refMatch[1].trim();
|
|
47
|
+
return {
|
|
48
|
+
id: `inline:${label}`,
|
|
49
|
+
label,
|
|
50
|
+
displayCode: codeString.split("\n").slice(1).join("\n"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { id: null, label: `${language} snippet`, displayCode: codeString };
|
|
54
|
+
}, [codeString, language]);
|
|
55
|
+
|
|
56
|
+
// Stable UUID for blocks without an explicit @ref
|
|
57
|
+
const stableIdRef = useRef(`inline:${crypto.randomUUID()}`);
|
|
58
|
+
const finalId = parsed.id ?? stableIdRef.current;
|
|
59
|
+
|
|
60
|
+
// Register on mount when there is an explicit @ref so inlineref:// links resolve.
|
|
61
|
+
// Guard: skip the store update if the entry is already registered with the same
|
|
62
|
+
// content — this prevents an infinite render loop when the component remounts
|
|
63
|
+
// because a parent re-renders (each store update would otherwise remount this,
|
|
64
|
+
// triggering the effect again, updating the store again…).
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!parsed.id) return;
|
|
67
|
+
const existing = useStore.getState().inlineCodeSnippets[parsed.id];
|
|
68
|
+
if (
|
|
69
|
+
existing?.content === parsed.displayCode &&
|
|
70
|
+
existing?.language === language
|
|
71
|
+
)
|
|
72
|
+
return;
|
|
73
|
+
registerInlineCode(parsed.id, {
|
|
74
|
+
content: parsed.displayCode,
|
|
75
|
+
language,
|
|
76
|
+
label: parsed.label,
|
|
77
|
+
});
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, [parsed.id]);
|
|
80
|
+
|
|
81
|
+
const handlePopout = () => {
|
|
82
|
+
registerInlineCode(finalId, {
|
|
83
|
+
content: parsed.displayCode,
|
|
84
|
+
language,
|
|
85
|
+
label: parsed.label,
|
|
86
|
+
});
|
|
87
|
+
openFileViewer(finalId);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleRun = () => {
|
|
91
|
+
const runLang =
|
|
92
|
+
language === "typescript" || language === "tsx"
|
|
93
|
+
? "typescript"
|
|
94
|
+
: "javascript";
|
|
95
|
+
openCodeRunner(parsed.displayCode, runLang);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleSaveAi = async () => {
|
|
99
|
+
if (!currentQuestion || savedAi) return;
|
|
100
|
+
await saveCodeSnippetToQuestion(
|
|
101
|
+
currentQuestion.id,
|
|
102
|
+
parsed.displayCode,
|
|
103
|
+
language,
|
|
104
|
+
parsed.label,
|
|
105
|
+
"ai",
|
|
106
|
+
);
|
|
107
|
+
setSavedAi(true);
|
|
108
|
+
setTimeout(() => setSavedAi(false), 2000);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="relative group/codeblock">
|
|
113
|
+
<div className="absolute right-1.5 top-1.5 z-10 flex items-center gap-0.5 opacity-0 group-hover/codeblock:opacity-100 transition-all">
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={handleRun}
|
|
117
|
+
className="p-1 rounded bg-slate-700/80 hover:bg-slate-600/90 text-slate-400 hover:text-emerald-400 transition-colors"
|
|
118
|
+
title="Run in code runner"
|
|
119
|
+
>
|
|
120
|
+
<Play className="w-3.5 h-3.5" />
|
|
121
|
+
</button>
|
|
122
|
+
{currentQuestion && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={handleSaveAi}
|
|
126
|
+
className={`p-1 rounded bg-slate-700/80 hover:bg-slate-600/90 transition-colors ${
|
|
127
|
+
savedAi ? "text-cyan-400" : "text-slate-400 hover:text-cyan-400"
|
|
128
|
+
}`}
|
|
129
|
+
title="Save to question context"
|
|
130
|
+
>
|
|
131
|
+
{savedAi ? (
|
|
132
|
+
<Check className="w-3.5 h-3.5" />
|
|
133
|
+
) : (
|
|
134
|
+
<Save className="w-3.5 h-3.5" />
|
|
135
|
+
)}
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={handlePopout}
|
|
141
|
+
className="p-1 rounded bg-slate-700/80 hover:bg-slate-600/90 text-slate-400 hover:text-violet-400 transition-colors"
|
|
142
|
+
title="Open in code viewer"
|
|
143
|
+
>
|
|
144
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
<SyntaxHighlighter
|
|
148
|
+
style={oneDark}
|
|
149
|
+
language={language}
|
|
150
|
+
PreTag="div"
|
|
151
|
+
customStyle={{
|
|
152
|
+
margin: "0.5rem 0",
|
|
153
|
+
borderRadius: "0.5rem",
|
|
154
|
+
fontSize: "0.8rem",
|
|
155
|
+
background: "#1e293b",
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{parsed.displayCode}
|
|
159
|
+
</SyntaxHighlighter>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
18
162
|
}
|
|
19
163
|
|
|
20
164
|
const markdownComponents: Components = {
|
|
@@ -35,6 +179,10 @@ const markdownComponents: Components = {
|
|
|
35
179
|
return <MermaidDiagram chart={codeString} />;
|
|
36
180
|
}
|
|
37
181
|
|
|
182
|
+
if (lang === "plot" || lang === "vega" || lang === "vega-lite") {
|
|
183
|
+
return <PlotEmbed spec={codeString} />;
|
|
184
|
+
}
|
|
185
|
+
|
|
38
186
|
if (lang === "viz") {
|
|
39
187
|
return <VizCraftEmbed spec={codeString} />;
|
|
40
188
|
}
|
|
@@ -179,8 +327,10 @@ export default function MarkdownRenderer({
|
|
|
179
327
|
onAnnotationClick,
|
|
180
328
|
bookmarkedBlockIndex,
|
|
181
329
|
onBookmarkBlock,
|
|
330
|
+
onSpecRefined,
|
|
182
331
|
}: Props) {
|
|
183
332
|
const openFileViewer = useStore((s) => s.openFileViewer);
|
|
333
|
+
const openDocViewer = useStore((s) => s.openDocViewer);
|
|
184
334
|
const bookmarkElemRef = useRef<HTMLDivElement | null>(null);
|
|
185
335
|
|
|
186
336
|
// Scroll the bookmarked block into view when the bookmark changes or on mount
|
|
@@ -237,6 +387,27 @@ export default function MarkdownRenderer({
|
|
|
237
387
|
|
|
238
388
|
return {
|
|
239
389
|
...markdownComponents,
|
|
390
|
+
// Override the code renderer so viz blocks get the onSpecRefined callback
|
|
391
|
+
code({ className, children, ...props }) {
|
|
392
|
+
const match = /language-(\w+)/.exec(className || "");
|
|
393
|
+
const codeString = String(children).replace(/\n$/, "");
|
|
394
|
+
if (match) {
|
|
395
|
+
const lang = match[1];
|
|
396
|
+
if (lang === "mermaid") return <MermaidDiagram chart={codeString} />;
|
|
397
|
+
if (lang === "viz")
|
|
398
|
+
return (
|
|
399
|
+
<VizCraftEmbed spec={codeString} onSpecRefined={onSpecRefined} />
|
|
400
|
+
);
|
|
401
|
+
// Fenced code block: render with pop-out button
|
|
402
|
+
return <InlineCodeBlock codeString={codeString} language={lang} />;
|
|
403
|
+
}
|
|
404
|
+
// Delegate to the static handler for inline code / unsupported langs
|
|
405
|
+
return (markdownComponents.code as Function)({
|
|
406
|
+
className,
|
|
407
|
+
children,
|
|
408
|
+
...props,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
240
411
|
p({ children, node }) {
|
|
241
412
|
return wrapBlock(
|
|
242
413
|
(node as any)?.position?.start?.line,
|
|
@@ -288,6 +459,35 @@ export default function MarkdownRenderer({
|
|
|
288
459
|
</span>
|
|
289
460
|
);
|
|
290
461
|
}
|
|
462
|
+
if (href?.startsWith("docref://")) {
|
|
463
|
+
const withoutScheme = href.slice("docref://".length);
|
|
464
|
+
const qIdx = withoutScheme.indexOf("?");
|
|
465
|
+
const fileId =
|
|
466
|
+
qIdx === -1 ? withoutScheme : withoutScheme.slice(0, qIdx);
|
|
467
|
+
const params = new URLSearchParams(
|
|
468
|
+
qIdx === -1 ? "" : withoutScheme.slice(qIdx + 1),
|
|
469
|
+
);
|
|
470
|
+
const fileName = params.has("n")
|
|
471
|
+
? decodeURIComponent(params.get("n")!)
|
|
472
|
+
: "Document";
|
|
473
|
+
const quote = String(
|
|
474
|
+
typeof children === "string"
|
|
475
|
+
? children
|
|
476
|
+
: Array.isArray(children)
|
|
477
|
+
? children.join("")
|
|
478
|
+
: "",
|
|
479
|
+
).trim();
|
|
480
|
+
return (
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onClick={() => openDocViewer(fileId, quote, fileName)}
|
|
484
|
+
className="inline-flex items-center gap-1 text-emerald-400 hover:text-emerald-300 underline decoration-emerald-500/50 underline-offset-2 transition-colors text-[0.85em]"
|
|
485
|
+
title={`View in ${fileName}`}
|
|
486
|
+
>
|
|
487
|
+
{children}
|
|
488
|
+
</button>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
291
491
|
if (href?.startsWith("coderef://")) {
|
|
292
492
|
const filePath = href.slice("coderef://".length);
|
|
293
493
|
return (
|
|
@@ -301,6 +501,19 @@ export default function MarkdownRenderer({
|
|
|
301
501
|
</button>
|
|
302
502
|
);
|
|
303
503
|
}
|
|
504
|
+
if (href?.startsWith("inlineref://")) {
|
|
505
|
+
const id = `inline:${href.slice("inlineref://".length)}`;
|
|
506
|
+
return (
|
|
507
|
+
<button
|
|
508
|
+
type="button"
|
|
509
|
+
onClick={() => openFileViewer(id)}
|
|
510
|
+
className="inline-flex items-center gap-1 text-violet-400 hover:text-violet-300 underline decoration-violet-500/50 underline-offset-2 transition-colors font-mono text-[0.8em]"
|
|
511
|
+
title="Open inline code block"
|
|
512
|
+
>
|
|
513
|
+
{children}
|
|
514
|
+
</button>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
304
517
|
return (
|
|
305
518
|
<a
|
|
306
519
|
href={href}
|
|
@@ -316,8 +529,10 @@ export default function MarkdownRenderer({
|
|
|
316
529
|
}, [
|
|
317
530
|
onAnnotationClick,
|
|
318
531
|
openFileViewer,
|
|
532
|
+
openDocViewer,
|
|
319
533
|
bookmarkedBlockIndex,
|
|
320
534
|
onBookmarkBlock,
|
|
535
|
+
onSpecRefined,
|
|
321
536
|
]);
|
|
322
537
|
|
|
323
538
|
return (
|
|
@@ -328,6 +543,8 @@ export default function MarkdownRenderer({
|
|
|
328
543
|
// Allow our internal schemes; block javascript: for safety
|
|
329
544
|
if (url.startsWith("annot://")) return url;
|
|
330
545
|
if (url.startsWith("coderef://")) return url;
|
|
546
|
+
if (url.startsWith("docref://")) return url;
|
|
547
|
+
if (url.startsWith("inlineref://")) return url;
|
|
331
548
|
if (url.startsWith("javascript:")) return "";
|
|
332
549
|
return url;
|
|
333
550
|
}}
|