App.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { useState, useEffect } from "react";
  2. import { useHashRoute } from "./hooks/use-hash-route";
  3. import type { HashRoute } from "./hooks/use-hash-route";
  4. import { Sidebar } from "./components/Sidebar";
  5. import { Dashboard } from "./pages/Dashboard";
  6. import { ChatPage } from "./pages/ChatPage";
  7. import { ChapterReader } from "./pages/ChapterReader";
  8. import { Analytics } from "./pages/Analytics";
  9. import { ServiceListPage } from "./pages/ServiceListPage";
  10. import { ServiceDetailPage } from "./pages/ServiceDetailPage";
  11. import { TruthFiles } from "./pages/TruthFiles";
  12. import { DaemonControl } from "./pages/DaemonControl";
  13. import { LogViewer } from "./pages/LogViewer";
  14. import { GenreManager } from "./pages/GenreManager";
  15. import { StyleManager } from "./pages/StyleManager";
  16. import { ImportManager } from "./pages/ImportManager";
  17. import { RadarView } from "./pages/RadarView";
  18. import { DoctorView } from "./pages/DoctorView";
  19. import { LanguageSelector } from "./pages/LanguageSelector";
  20. import { BookSidebar, BookSidebarToggle } from "./components/chat/BookSidebar";
  21. import { useSSE } from "./hooks/use-sse";
  22. import { useSessionEvents } from "./hooks/use-session-events";
  23. import { useTheme } from "./hooks/use-theme";
  24. import { useI18n } from "./hooks/use-i18n";
  25. import { postApi, putApi, useApi } from "./hooks/use-api";
  26. import { Sun, Moon } from "lucide-react";
  27. import { House } from "lucide-react";
  28. export type { HashRoute as Route } from "./hooks/use-hash-route";
  29. export function deriveActiveBookId(route: HashRoute): string | undefined {
  30. if ("bookId" in route) return route.bookId;
  31. return undefined;
  32. }
  33. export function App() {
  34. const { route, setRoute } = useHashRoute();
  35. const sse = useSSE();
  36. const { theme, setTheme } = useTheme();
  37. const { t, lang: currentLang } = useI18n();
  38. const { data: project, refetch: refetchProject } = useApi<{ language: string; languageExplicit: boolean }>("/project");
  39. const [showLanguageSelector, setShowLanguageSelector] = useState(false);
  40. const [ready, setReady] = useState(false);
  41. const isDark = theme === "dark";
  42. useEffect(() => {
  43. document.documentElement.classList.toggle("dark", isDark);
  44. }, [isDark]);
  45. useEffect(() => {
  46. if (project) {
  47. if (!project.languageExplicit) {
  48. setShowLanguageSelector(true);
  49. }
  50. setReady(true);
  51. }
  52. }, [project]);
  53. useSessionEvents(sse, route, setRoute);
  54. const nav = {
  55. toDashboard: () => setRoute({ page: "dashboard" }),
  56. toBook: (bookId: string) => setRoute({ page: "book", bookId }),
  57. toBookCreate: () => setRoute({ page: "book-create" }),
  58. toChapter: (bookId: string, chapterNumber: number) =>
  59. setRoute({ page: "chapter", bookId, chapterNumber }),
  60. toAnalytics: (bookId: string) => setRoute({ page: "analytics", bookId }),
  61. toServices: () => setRoute({ page: "services" }),
  62. toServiceDetail: (id: string) => setRoute({ page: "service-detail", serviceId: id }),
  63. toTruth: (bookId: string) => setRoute({ page: "truth", bookId }),
  64. toDaemon: () => setRoute({ page: "daemon" }),
  65. toLogs: () => setRoute({ page: "logs" }),
  66. toGenres: () => setRoute({ page: "genres" }),
  67. toStyle: () => setRoute({ page: "style" }),
  68. toImport: () => setRoute({ page: "import" }),
  69. toRadar: () => setRoute({ page: "radar" }),
  70. toDoctor: () => setRoute({ page: "doctor" }),
  71. };
  72. const activeBookId = deriveActiveBookId(route);
  73. const activePage =
  74. activeBookId
  75. ? `book:${activeBookId}`
  76. : route.page === "service-detail"
  77. ? "services"
  78. : route.page;
  79. if (!ready) {
  80. return (
  81. <div className="min-h-screen bg-background flex items-center justify-center">
  82. <div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
  83. </div>
  84. );
  85. }
  86. if (showLanguageSelector) {
  87. return (
  88. <LanguageSelector
  89. onSelect={async (lang) => {
  90. await postApi("/project/language", { language: lang });
  91. setShowLanguageSelector(false);
  92. refetchProject();
  93. }}
  94. />
  95. );
  96. }
  97. return (
  98. <div className="h-screen bg-background text-foreground flex overflow-hidden font-sans">
  99. {/* Left Sidebar */}
  100. <Sidebar nav={nav} activePage={activePage} sse={sse} t={t} />
  101. {/* Center Content */}
  102. <div className="flex-1 flex flex-col min-w-0 bg-background/30 backdrop-blur-sm">
  103. {/* Header Strip */}
  104. <header className="h-14 shrink-0 flex items-center justify-between px-8 border-b border-border/40">
  105. <div className="flex items-center gap-2">
  106. <button
  107. onClick={nav.toDashboard}
  108. className="inline-flex items-center gap-2 rounded-lg border border-border/50 bg-card/70 px-3 py-1.5 text-sm font-semibold text-foreground hover:bg-secondary/50 transition-colors"
  109. >
  110. <House size={14} />
  111. <span>首页</span>
  112. <span className="text-muted-foreground/70">/</span>
  113. <span className="font-serif">InkOS Studio</span>
  114. </button>
  115. </div>
  116. <div className="flex items-center gap-3">
  117. <div className="flex gap-0.5 bg-muted/50 rounded-md p-0.5">
  118. <button
  119. onClick={async () => {
  120. await putApi("/project", { language: "zh" });
  121. refetchProject();
  122. }}
  123. className={`text-xs px-2 py-0.5 rounded ${currentLang === "zh" ? "bg-primary text-primary-foreground" : "text-muted-foreground"}`}
  124. >
  125. 中
  126. </button>
  127. <button
  128. onClick={async () => {
  129. await putApi("/project", { language: "en" });
  130. refetchProject();
  131. }}
  132. className={`text-xs px-2 py-0.5 rounded ${currentLang === "en" ? "bg-primary text-primary-foreground" : "text-muted-foreground"}`}
  133. >
  134. EN
  135. </button>
  136. </div>
  137. <button
  138. onClick={() => setTheme(isDark ? "light" : "dark")}
  139. className="text-muted-foreground hover:text-foreground transition-colors"
  140. >
  141. {isDark ? <Sun size={14} /> : <Moon size={14} />}
  142. </button>
  143. </div>
  144. </header>
  145. {/* Main Content Area */}
  146. <main className="flex-1 relative overflow-y-auto scroll-smooth">
  147. {route.page === "dashboard" && (
  148. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  149. <Dashboard nav={nav} sse={sse} theme={theme} t={t} />
  150. </div>
  151. )}
  152. {(route.page === "book" || route.page === "book-create") && (
  153. <div className="absolute inset-0 flex min-w-0">
  154. <ChatPage
  155. activeBookId={route.page === "book" ? route.bookId : undefined}
  156. nav={nav}
  157. theme={theme}
  158. t={t}
  159. sse={sse}
  160. />
  161. {route.page === "book" && (
  162. <>
  163. <BookSidebar bookId={route.bookId} theme={theme} t={t} sse={sse} />
  164. <BookSidebarToggle bookId={route.bookId} theme={theme} t={t} sse={sse} />
  165. </>
  166. )}
  167. </div>
  168. )}
  169. {route.page === "chapter" && (
  170. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  171. <ChapterReader bookId={route.bookId} chapterNumber={route.chapterNumber} nav={nav} theme={theme} t={t} />
  172. </div>
  173. )}
  174. {route.page === "analytics" && (
  175. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  176. <Analytics bookId={route.bookId} nav={nav} theme={theme} t={t} />
  177. </div>
  178. )}
  179. {route.page === "services" && (
  180. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  181. <ServiceListPage nav={nav} />
  182. </div>
  183. )}
  184. {route.page === "service-detail" && (
  185. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  186. <ServiceDetailPage serviceId={route.serviceId} nav={nav} />
  187. </div>
  188. )}
  189. {route.page === "truth" && (
  190. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  191. <TruthFiles bookId={route.bookId} nav={nav} theme={theme} t={t} />
  192. </div>
  193. )}
  194. {route.page === "daemon" && (
  195. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  196. <DaemonControl nav={nav} theme={theme} t={t} sse={sse} />
  197. </div>
  198. )}
  199. {route.page === "logs" && (
  200. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  201. <LogViewer nav={nav} theme={theme} t={t} />
  202. </div>
  203. )}
  204. {route.page === "genres" && (
  205. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  206. <GenreManager nav={nav} theme={theme} t={t} />
  207. </div>
  208. )}
  209. {route.page === "style" && (
  210. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  211. <StyleManager nav={nav} theme={theme} t={t} />
  212. </div>
  213. )}
  214. {route.page === "import" && (
  215. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  216. <ImportManager nav={nav} theme={theme} t={t} />
  217. </div>
  218. )}
  219. {route.page === "radar" && (
  220. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  221. <RadarView nav={nav} theme={theme} t={t} />
  222. </div>
  223. )}
  224. {route.page === "doctor" && (
  225. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  226. <DoctorView nav={nav} theme={theme} t={t} />
  227. </div>
  228. )}
  229. </main>
  230. </div>
  231. </div>
  232. );
  233. }