App.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { useState, useEffect } from "react";
  2. import { Sidebar } from "./components/Sidebar";
  3. import { ChatPanel } from "./components/ChatBar";
  4. import { Dashboard } from "./pages/Dashboard";
  5. import { BookDetail } from "./pages/BookDetail";
  6. import { BookCreate } from "./pages/BookCreate";
  7. import { ChapterReader } from "./pages/ChapterReader";
  8. import { Analytics } from "./pages/Analytics";
  9. import { ConfigView } from "./pages/ConfigView";
  10. import { TruthFiles } from "./pages/TruthFiles";
  11. import { DaemonControl } from "./pages/DaemonControl";
  12. import { LogViewer } from "./pages/LogViewer";
  13. import { GenreManager } from "./pages/GenreManager";
  14. import { StyleManager } from "./pages/StyleManager";
  15. import { ImportManager } from "./pages/ImportManager";
  16. import { RadarView } from "./pages/RadarView";
  17. import { DoctorView } from "./pages/DoctorView";
  18. import { LanguageSelector } from "./pages/LanguageSelector";
  19. import { useSSE } from "./hooks/use-sse";
  20. import { useTheme } from "./hooks/use-theme";
  21. import { useI18n } from "./hooks/use-i18n";
  22. import { postApi, useApi } from "./hooks/use-api";
  23. import { Sun, Moon, Bell, MessageSquare } from "lucide-react";
  24. import { DEFAULT_CHAT_OPEN } from "./app-state";
  25. export type Route =
  26. | { page: "dashboard" }
  27. | { page: "book"; bookId: string }
  28. | { page: "book-create" }
  29. | { page: "chapter"; bookId: string; chapterNumber: number }
  30. | { page: "analytics"; bookId: string }
  31. | { page: "config" }
  32. | { page: "truth"; bookId: string }
  33. | { page: "daemon" }
  34. | { page: "logs" }
  35. | { page: "genres" }
  36. | { page: "style" }
  37. | { page: "import" }
  38. | { page: "radar" }
  39. | { page: "doctor" };
  40. export function deriveActiveBookId(route: Route): string | undefined {
  41. return route.page === "book" || route.page === "chapter" || route.page === "truth" || route.page === "analytics"
  42. ? route.bookId
  43. : undefined;
  44. }
  45. export function App() {
  46. const [route, setRoute] = useState<Route>({ page: "dashboard" });
  47. const sse = useSSE();
  48. const { theme, setTheme } = useTheme();
  49. const { t } = useI18n();
  50. const { data: project, refetch: refetchProject } = useApi<{ language: string; languageExplicit: boolean }>("/project");
  51. const [showLanguageSelector, setShowLanguageSelector] = useState(false);
  52. const [ready, setReady] = useState(false);
  53. const [chatOpen, setChatOpen] = useState(DEFAULT_CHAT_OPEN);
  54. const isDark = theme === "dark";
  55. useEffect(() => {
  56. document.documentElement.classList.toggle("dark", isDark);
  57. }, [isDark]);
  58. useEffect(() => {
  59. if (project) {
  60. if (!project.languageExplicit) {
  61. setShowLanguageSelector(true);
  62. }
  63. setReady(true);
  64. }
  65. }, [project]);
  66. const nav = {
  67. toDashboard: () => setRoute({ page: "dashboard" }),
  68. toBook: (bookId: string) => setRoute({ page: "book", bookId }),
  69. toBookCreate: () => setRoute({ page: "book-create" }),
  70. toChapter: (bookId: string, chapterNumber: number) =>
  71. setRoute({ page: "chapter", bookId, chapterNumber }),
  72. toAnalytics: (bookId: string) => setRoute({ page: "analytics", bookId }),
  73. toConfig: () => setRoute({ page: "config" }),
  74. toTruth: (bookId: string) => setRoute({ page: "truth", bookId }),
  75. toDaemon: () => setRoute({ page: "daemon" }),
  76. toLogs: () => setRoute({ page: "logs" }),
  77. toGenres: () => setRoute({ page: "genres" }),
  78. toStyle: () => setRoute({ page: "style" }),
  79. toImport: () => setRoute({ page: "import" }),
  80. toRadar: () => setRoute({ page: "radar" }),
  81. toDoctor: () => setRoute({ page: "doctor" }),
  82. };
  83. const activeBookId = deriveActiveBookId(route);
  84. const activePage =
  85. activeBookId
  86. ? `book:${activeBookId}`
  87. : route.page;
  88. if (!ready) {
  89. return (
  90. <div className="min-h-screen bg-background flex items-center justify-center">
  91. <div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
  92. </div>
  93. );
  94. }
  95. if (showLanguageSelector) {
  96. return (
  97. <LanguageSelector
  98. onSelect={async (lang) => {
  99. await postApi("/project/language", { language: lang });
  100. setShowLanguageSelector(false);
  101. refetchProject();
  102. }}
  103. />
  104. );
  105. }
  106. return (
  107. <div className="h-screen bg-background text-foreground flex overflow-hidden font-sans">
  108. {/* Left Sidebar */}
  109. <Sidebar nav={nav} activePage={activePage} sse={sse} t={t} />
  110. {/* Center Content */}
  111. <div className="flex-1 flex flex-col min-w-0 bg-background/30 backdrop-blur-sm">
  112. {/* Header Strip */}
  113. <header className="h-14 shrink-0 flex items-center justify-between px-8 border-b border-border/40">
  114. <div className="flex items-center gap-2">
  115. <span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-bold">
  116. InkOS Studio
  117. </span>
  118. </div>
  119. <div className="flex items-center gap-3">
  120. <button
  121. onClick={() => setTheme(isDark ? "light" : "dark")}
  122. className="w-8 h-8 flex items-center justify-center rounded-lg bg-secondary text-muted-foreground hover:text-primary hover:bg-primary/10 transition-all shadow-sm"
  123. title={isDark ? "Switch to Light Mode" : "Switch to Dark Mode"}
  124. >
  125. {isDark ? <Sun size={16} /> : <Moon size={16} />}
  126. </button>
  127. <button className="w-8 h-8 flex items-center justify-center rounded-lg bg-secondary text-muted-foreground hover:text-foreground transition-all relative">
  128. <Bell size={16} />
  129. <span className="absolute top-1 right-1 w-2 h-2 bg-primary rounded-full border-2 border-background" />
  130. </button>
  131. {/* Chat Panel Toggle */}
  132. <button
  133. onClick={() => setChatOpen((prev) => !prev)}
  134. className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all shadow-sm ${
  135. chatOpen
  136. ? "bg-primary text-primary-foreground shadow-primary/20"
  137. : "bg-secondary text-muted-foreground hover:text-primary hover:bg-primary/10"
  138. }`}
  139. title="Toggle AI Assistant"
  140. >
  141. <MessageSquare size={16} />
  142. </button>
  143. </div>
  144. </header>
  145. {/* Main Content Area */}
  146. <main className="flex-1 overflow-y-auto scroll-smooth">
  147. <div className="max-w-4xl mx-auto px-6 py-12 md:px-12 lg:py-16 fade-in">
  148. {route.page === "dashboard" && <Dashboard nav={nav} sse={sse} theme={theme} t={t} />}
  149. {route.page === "book" && <BookDetail bookId={route.bookId} nav={nav} theme={theme} t={t} sse={sse} />}
  150. {route.page === "book-create" && <BookCreate nav={nav} theme={theme} t={t} />}
  151. {route.page === "chapter" && <ChapterReader bookId={route.bookId} chapterNumber={route.chapterNumber} nav={nav} theme={theme} t={t} />}
  152. {route.page === "analytics" && <Analytics bookId={route.bookId} nav={nav} theme={theme} t={t} />}
  153. {route.page === "config" && <ConfigView nav={nav} theme={theme} t={t} />}
  154. {route.page === "truth" && <TruthFiles bookId={route.bookId} nav={nav} theme={theme} t={t} />}
  155. {route.page === "daemon" && <DaemonControl nav={nav} theme={theme} t={t} sse={sse} />}
  156. {route.page === "logs" && <LogViewer nav={nav} theme={theme} t={t} />}
  157. {route.page === "genres" && <GenreManager nav={nav} theme={theme} t={t} />}
  158. {route.page === "style" && <StyleManager nav={nav} theme={theme} t={t} />}
  159. {route.page === "import" && <ImportManager nav={nav} theme={theme} t={t} />}
  160. {route.page === "radar" && <RadarView nav={nav} theme={theme} t={t} />}
  161. {route.page === "doctor" && <DoctorView nav={nav} theme={theme} t={t} />}
  162. </div>
  163. </main>
  164. </div>
  165. {/* Right Chat Panel */}
  166. <ChatPanel
  167. open={chatOpen}
  168. onClose={() => setChatOpen(false)}
  169. t={t}
  170. sse={sse}
  171. activeBookId={activeBookId}
  172. />
  173. </div>
  174. );
  175. }