feat: Extract shared citation parser and formatTime utilities, add time…
- "frontend/src/utils/chatCitations.tsx" - "frontend/src/utils/formatTime.ts" - "frontend/src/api/chat.ts" - "frontend/src/pages/ChatPage.tsx" - "frontend/src/pages/ChatPage.module.css" - "frontend/src/components/ChatWidget.tsx" - "frontend/src/components/ChatWidget.module.css" GSD-Task: S05/T02
This commit is contained in:
parent
2f9e3272d9
commit
ecfdc76ba6
8 changed files with 211 additions and 129 deletions
|
|
@ -19,6 +19,10 @@ export interface ChatSource {
|
||||||
summary: string;
|
summary: string;
|
||||||
section_anchor: string;
|
section_anchor: string;
|
||||||
section_heading: string;
|
section_heading: string;
|
||||||
|
source_video_id?: string;
|
||||||
|
start_time?: number;
|
||||||
|
end_time?: number;
|
||||||
|
video_filename?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatDoneMeta {
|
export interface ChatDoneMeta {
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,49 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Source card content wrapper ───────────────────────────── */
|
||||||
|
|
||||||
|
.sourceContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Timestamp badge & video metadata ─────────────────────── */
|
||||||
|
|
||||||
|
.timestampBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1875rem;
|
||||||
|
padding: 0.0625rem 0.25rem;
|
||||||
|
background: rgba(var(--color-accent-rgb, 217, 186, 140), 0.12);
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestampBadge:hover {
|
||||||
|
background: rgba(var(--color-accent-rgb, 217, 186, 140), 0.22);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoMeta {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.5625rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive (mobile) ──────────────────────────────────── */
|
/* ── Responsive (mobile) ──────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,10 @@
|
||||||
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { streamChat, type ChatSource } from "../api/chat";
|
import { streamChat, type ChatSource } from "../api/chat";
|
||||||
|
import { parseChatCitations } from "../utils/chatCitations";
|
||||||
|
import { formatTime } from "../utils/formatTime";
|
||||||
import styles from "./ChatWidget.module.css";
|
import styles from "./ChatWidget.module.css";
|
||||||
|
|
||||||
// Same citation regex as ChatPage / utils/citations.tsx
|
|
||||||
const CITATION_RE = /\[(\d+(?:,\s*\d+)*)\]/g;
|
|
||||||
|
|
||||||
interface Technique {
|
interface Technique {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
@ -60,61 +59,6 @@ function buildSuggestions(creatorName: string, techniques: Technique[]): string[
|
||||||
return suggestions.slice(0, 3);
|
return suggestions.slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse citation markers [N] into React nodes with superscript links. */
|
|
||||||
function parseCitations(text: string, sources: ChatSource[]): React.ReactNode[] {
|
|
||||||
const nodes: React.ReactNode[] = [];
|
|
||||||
let lastIndex = 0;
|
|
||||||
|
|
||||||
for (const match of text.matchAll(CITATION_RE)) {
|
|
||||||
const matchStart = match.index ?? 0;
|
|
||||||
if (matchStart > lastIndex) {
|
|
||||||
nodes.push(text.slice(lastIndex, matchStart));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawGroup = match[1];
|
|
||||||
if (!rawGroup) continue;
|
|
||||||
const indices = rawGroup.split(",").map((s) => parseInt(s.trim(), 10));
|
|
||||||
const links: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < indices.length; i++) {
|
|
||||||
const idx = indices[i]!;
|
|
||||||
const source = sources[idx - 1];
|
|
||||||
if (source) {
|
|
||||||
if (i > 0) links.push(", ");
|
|
||||||
const href = source.section_anchor
|
|
||||||
? `/techniques/${source.slug}#${source.section_anchor}`
|
|
||||||
: `/techniques/${source.slug}`;
|
|
||||||
links.push(
|
|
||||||
<Link
|
|
||||||
key={`${matchStart}-${idx}`}
|
|
||||||
to={href}
|
|
||||||
className={styles.citationLink}
|
|
||||||
title={`${source.title}${source.section_heading ? ` — ${source.section_heading}` : ""}`}
|
|
||||||
>
|
|
||||||
{idx}
|
|
||||||
</Link>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (i > 0) links.push(", ");
|
|
||||||
links.push(String(idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.push(
|
|
||||||
<sup key={`cite-${matchStart}`} className={styles.citationGroup}>
|
|
||||||
[{links}]
|
|
||||||
</sup>,
|
|
||||||
);
|
|
||||||
lastIndex = matchStart + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
nodes.push(text.slice(lastIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes.length > 0 ? nodes : [text];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map personality weight to a human-readable tier label. */
|
/** Map personality weight to a human-readable tier label. */
|
||||||
function getTierLabel(weight: number): string {
|
function getTierLabel(weight: number): string {
|
||||||
if (weight < 0.2) return "Encyclopedic";
|
if (weight < 0.2) return "Encyclopedic";
|
||||||
|
|
@ -339,7 +283,7 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
|
||||||
<div className={styles.errorMsg}>{msg.error}</div>
|
<div className={styles.errorMsg}>{msg.error}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{parseCitations(msg.text, msg.sources)}
|
{parseChatCitations(msg.text, msg.sources, styles)}
|
||||||
{!msg.done && <span className={styles.cursor} />}
|
{!msg.done && <span className={styles.cursor} />}
|
||||||
{msg.done && msg.sources.length > 0 && (
|
{msg.done && msg.sources.length > 0 && (
|
||||||
<div className={styles.sources}>
|
<div className={styles.sources}>
|
||||||
|
|
@ -352,10 +296,25 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
|
||||||
return (
|
return (
|
||||||
<li key={src.number} className={styles.sourceItem}>
|
<li key={src.number} className={styles.sourceItem}>
|
||||||
<span className={styles.sourceNum}>{src.number}</span>
|
<span className={styles.sourceNum}>{src.number}</span>
|
||||||
|
<div className={styles.sourceContent}>
|
||||||
<Link to={href} className={styles.sourceLink}>
|
<Link to={href} className={styles.sourceLink}>
|
||||||
{src.title}
|
{src.title}
|
||||||
{src.section_heading ? ` — ${src.section_heading}` : ""}
|
{src.section_heading ? ` — ${src.section_heading}` : ""}
|
||||||
</Link>
|
</Link>
|
||||||
|
{src.start_time != null && src.source_video_id && (
|
||||||
|
<Link
|
||||||
|
to={`/watch/${src.source_video_id}?t=${Math.floor(src.start_time)}`}
|
||||||
|
className={styles.timestampBadge}
|
||||||
|
title="Jump to video timestamp"
|
||||||
|
>
|
||||||
|
▶ {formatTime(src.start_time)}
|
||||||
|
{src.end_time != null && `–${formatTime(src.end_time)}`}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{src.video_filename && (
|
||||||
|
<span className={styles.videoMeta}>{src.video_filename}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,36 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Timestamp badge & video metadata ─────────────────────── */
|
||||||
|
|
||||||
|
.timestampBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
background: rgba(var(--color-accent-rgb, 217, 186, 140), 0.12);
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestampBadge:hover {
|
||||||
|
background: rgba(var(--color-accent-rgb, 217, 186, 140), 0.22);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoMeta {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Citation superscript links in text ───────────────────── */
|
/* ── Citation superscript links in text ───────────────────── */
|
||||||
|
|
||||||
.citationGroup {
|
.citationGroup {
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,11 @@
|
||||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { streamChat, type ChatSource } from "../api/chat";
|
import { streamChat, type ChatSource } from "../api/chat";
|
||||||
|
import { parseChatCitations } from "../utils/chatCitations";
|
||||||
|
import { formatTime } from "../utils/formatTime";
|
||||||
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||||
import styles from "./ChatPage.module.css";
|
import styles from "./ChatPage.module.css";
|
||||||
|
|
||||||
// Matches [1], [2,3], [1,2,3] etc. — same regex as utils/citations.tsx
|
|
||||||
const CITATION_RE = /\[(\d+(?:,\s*\d+)*)\]/g;
|
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant";
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -24,67 +23,6 @@ interface Message {
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse text containing [N] markers into React nodes with citation links.
|
|
||||||
*/
|
|
||||||
function parseChatCitations(
|
|
||||||
text: string,
|
|
||||||
sources: ChatSource[],
|
|
||||||
): React.ReactNode[] {
|
|
||||||
const nodes: React.ReactNode[] = [];
|
|
||||||
let lastIndex = 0;
|
|
||||||
|
|
||||||
for (const match of text.matchAll(CITATION_RE)) {
|
|
||||||
const matchStart = match.index ?? 0;
|
|
||||||
if (matchStart > lastIndex) {
|
|
||||||
nodes.push(text.slice(lastIndex, matchStart));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawGroup = match[1];
|
|
||||||
if (!rawGroup) continue;
|
|
||||||
const indices = rawGroup.split(",").map((s) => parseInt(s.trim(), 10));
|
|
||||||
const links: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < indices.length; i++) {
|
|
||||||
const idx = indices[i]!;
|
|
||||||
const source = sources[idx - 1]; // 1-based
|
|
||||||
if (source) {
|
|
||||||
if (i > 0) links.push(", ");
|
|
||||||
const href = source.section_anchor
|
|
||||||
? `/techniques/${source.slug}#${source.section_anchor}`
|
|
||||||
: `/techniques/${source.slug}`;
|
|
||||||
links.push(
|
|
||||||
<Link
|
|
||||||
key={`${matchStart}-${idx}`}
|
|
||||||
to={href}
|
|
||||||
className={styles.citationLink}
|
|
||||||
title={`${source.title}${source.section_heading ? ` — ${source.section_heading}` : ""}`}
|
|
||||||
>
|
|
||||||
{idx}
|
|
||||||
</Link>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (i > 0) links.push(", ");
|
|
||||||
links.push(String(idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.push(
|
|
||||||
<sup key={`cite-${matchStart}`} className={styles.citationGroup}>
|
|
||||||
[{links}]
|
|
||||||
</sup>,
|
|
||||||
);
|
|
||||||
|
|
||||||
lastIndex = matchStart + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
nodes.push(text.slice(lastIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes.length > 0 ? nodes : [text];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
useDocumentTitle("Chat — Chrysopedia");
|
useDocumentTitle("Chat — Chrysopedia");
|
||||||
|
|
||||||
|
|
@ -244,7 +182,7 @@ export default function ChatPage() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styles.responseText}>
|
<div className={styles.responseText}>
|
||||||
{parseChatCitations(msg.text, msg.sources)}
|
{parseChatCitations(msg.text, msg.sources, styles)}
|
||||||
{!msg.done && <span className={styles.cursor} />}
|
{!msg.done && <span className={styles.cursor} />}
|
||||||
</div>
|
</div>
|
||||||
{msg.done && msg.sources.length > 0 && (
|
{msg.done && msg.sources.length > 0 && (
|
||||||
|
|
@ -273,12 +211,28 @@ export default function ChatPage() {
|
||||||
? ` — ${src.section_heading}`
|
? ` — ${src.section_heading}`
|
||||||
: ""}
|
: ""}
|
||||||
</Link>
|
</Link>
|
||||||
|
{src.start_time != null && src.source_video_id && (
|
||||||
|
<Link
|
||||||
|
to={`/watch/${src.source_video_id}?t=${Math.floor(src.start_time)}`}
|
||||||
|
className={styles.timestampBadge}
|
||||||
|
title="Jump to video timestamp"
|
||||||
|
>
|
||||||
|
▶ {formatTime(src.start_time)}
|
||||||
|
{src.end_time != null && `–${formatTime(src.end_time)}`}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
{src.creator_name && (
|
{src.creator_name && (
|
||||||
<span className={styles.sourceMeta}>
|
<span className={styles.sourceMeta}>
|
||||||
{" "}
|
{" "}
|
||||||
· {src.creator_name}
|
· {src.creator_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{src.video_filename && (
|
||||||
|
<span className={styles.videoMeta}>
|
||||||
|
{" "}
|
||||||
|
· {src.video_filename}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
77
frontend/src/utils/chatCitations.tsx
Normal file
77
frontend/src/utils/chatCitations.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* Shared chat citation parser for ChatPage and ChatWidget.
|
||||||
|
*
|
||||||
|
* Parses [N] and [N,M] markers in streamed chat text into superscript
|
||||||
|
* <Link> elements pointing to technique pages with optional section anchors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import type { ChatSource } from "../api/chat";
|
||||||
|
|
||||||
|
/** Matches [1], [2,3], [1,2,3] etc. */
|
||||||
|
const CITATION_RE = /\[(\d+(?:,\s*\d+)*)\]/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse text containing [N] markers into React nodes with citation links.
|
||||||
|
* Takes a styles object so both ChatPage and ChatWidget can pass their own CSS modules.
|
||||||
|
* Expects `citationGroup` and `citationLink` keys in the styles object.
|
||||||
|
*/
|
||||||
|
export function parseChatCitations(
|
||||||
|
text: string,
|
||||||
|
sources: ChatSource[],
|
||||||
|
styles: Record<string, string>,
|
||||||
|
): React.ReactNode[] {
|
||||||
|
const nodes: React.ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
for (const match of text.matchAll(CITATION_RE)) {
|
||||||
|
const matchStart = match.index ?? 0;
|
||||||
|
if (matchStart > lastIndex) {
|
||||||
|
nodes.push(text.slice(lastIndex, matchStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawGroup = match[1];
|
||||||
|
if (!rawGroup) continue;
|
||||||
|
const indices = rawGroup.split(",").map((s) => parseInt(s.trim(), 10));
|
||||||
|
const links: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < indices.length; i++) {
|
||||||
|
const idx = indices[i]!;
|
||||||
|
const source = sources[idx - 1]; // 1-based
|
||||||
|
if (source) {
|
||||||
|
if (i > 0) links.push(", ");
|
||||||
|
const href = source.section_anchor
|
||||||
|
? `/techniques/${source.slug}#${source.section_anchor}`
|
||||||
|
: `/techniques/${source.slug}`;
|
||||||
|
links.push(
|
||||||
|
<Link
|
||||||
|
key={`${matchStart}-${idx}`}
|
||||||
|
to={href}
|
||||||
|
className={styles.citationLink}
|
||||||
|
title={`${source.title}${source.section_heading ? ` — ${source.section_heading}` : ""}`}
|
||||||
|
>
|
||||||
|
{idx}
|
||||||
|
</Link>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (i > 0) links.push(", ");
|
||||||
|
links.push(String(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
<sup key={`cite-${matchStart}`} className={styles.citationGroup}>
|
||||||
|
[{links}]
|
||||||
|
</sup>,
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = matchStart + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
nodes.push(text.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.length > 0 ? nodes : [text];
|
||||||
|
}
|
||||||
15
frontend/src/utils/formatTime.ts
Normal file
15
frontend/src/utils/formatTime.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Format a duration in seconds as M:SS (< 1 hour) or H:MM:SS (>= 1 hour).
|
||||||
|
* Used by player controls, chapter markers, key moment badges, and chat source cards.
|
||||||
|
*/
|
||||||
|
export function formatTime(seconds: number): string {
|
||||||
|
const total = Math.floor(seconds);
|
||||||
|
const h = Math.floor(total / 3600);
|
||||||
|
const m = Math.floor((total % 3600) / 60);
|
||||||
|
const s = total % 60;
|
||||||
|
|
||||||
|
if (h > 0) {
|
||||||
|
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/templates.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/EmbedPlayer.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx","./src/utils/clipboard.ts"],"version":"5.6.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/shorts.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/templates.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/PostsFeed.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/EmbedPlayer.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/PostsList.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/ShortPlayer.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/chatCitations.tsx","./src/utils/citations.tsx","./src/utils/clipboard.ts","./src/utils/formatTime.ts"],"version":"5.6.3"}
|
||||||
Loading…
Add table
Reference in a new issue