mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-02-17 19:54:23 +01:00
Next.js App separates vite SPA and hono BE
This commit is contained in:
2
.env.local.sample
Normal file
2
.env.local.sample
Normal file
@@ -0,0 +1,2 @@
|
||||
DEV_FE_PORT=3400
|
||||
DEV_BE_PORT=3401
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
Claude Code Viewer reads Claude Code session logs directly from JSONL files (`~/.claude/projects/`) with zero data loss. It's a web-based client built as a CLI tool serving a Next.js application.
|
||||
Claude Code Viewer reads Claude Code session logs directly from JSONL files (`~/.claude/projects/`) with zero data loss. It's a web-based client built as a CLI tool serving a Vite application.
|
||||
|
||||
**Core Architecture**:
|
||||
- Frontend: Next.js 15 + React 19 + TanStack Query
|
||||
- Backend: Hono + Effect-TS (all business logic)
|
||||
- Frontend: Vite + TanStack Router + React 19 + TanStack Query
|
||||
- Backend: Hono (standalone server) + Effect-TS (all business logic)
|
||||
- Data: Direct JSONL reads with strict Zod validation
|
||||
- Real-time: Server-Sent Events (SSE) for live updates
|
||||
|
||||
@@ -54,10 +54,11 @@ pnpm test
|
||||
|
||||
## Key Directory Patterns
|
||||
|
||||
- `src/app/api/[[...route]]/` - Hono API entry point (all routes defined here)
|
||||
- `src/server/hono/route.ts` - Hono API routes definition (all routes defined here)
|
||||
- `src/server/core/` - Effect-TS business logic (domain modules: session, project, git, etc.)
|
||||
- `src/lib/conversation-schema/` - Zod schemas for JSONL validation
|
||||
- `src/testing/layers/` - Reusable Effect test layers (`testPlatformLayer` is the foundation)
|
||||
- `src/routes/` - TanStack Router routes
|
||||
|
||||
## Coding Standards
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"!**/*.css",
|
||||
"!dist",
|
||||
"!playwright.config.ts",
|
||||
"!src/lib/i18n/locales/*/messages.ts"
|
||||
"!src/lib/i18n/locales/*/messages.ts",
|
||||
"!src/routeTree.gen.ts"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
@@ -51,6 +52,12 @@
|
||||
"linter": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.config.ts"],
|
||||
"linter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
||||
72
docs/dev.md
72
docs/dev.md
@@ -6,13 +6,13 @@ This document provides technical details for developers contributing to Claude C
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Framework**: Next.js 15 (App Router)
|
||||
- **Framework**: Vite + TanStack Router
|
||||
- **UI Libraries**: React 19, Radix UI, Tailwind CSS
|
||||
- **State Management**: Jotai (global state), TanStack Query (server state)
|
||||
|
||||
### Backend
|
||||
|
||||
- **API Framework**: Hono integrated with Next.js API Routes
|
||||
- **API Framework**: Hono (standalone server with @hono/node-server)
|
||||
- Type-safe communication via Hono RPC
|
||||
- Validation using `@hono/zod-validator`
|
||||
- **Effect-TS**: All backend business logic is implemented using Effect-TS
|
||||
@@ -61,27 +61,21 @@ pnpm install
|
||||
|
||||
## Starting the Development Server
|
||||
|
||||
### Development Mode (with limitations)
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The development server will start, but **with the following limitations**:
|
||||
This command starts:
|
||||
- Frontend: Vite development server (port 3400 by default)
|
||||
- Backend: Node server with tsx watch (port 3401 by default)
|
||||
|
||||
#### Next.js Development Server Constraints
|
||||
Both servers run simultaneously using `npm-run-all2` for parallel execution.
|
||||
|
||||
The Next.js development server behaves like an Edge Runtime where API Routes don't share memory space. This causes:
|
||||
### Production Mode
|
||||
|
||||
1. **Initialization runs on every request**: Initialization occurs for each API request, degrading performance
|
||||
2. **Session process continuation unavailable**: Cannot resume Paused sessions across different requests
|
||||
3. **SSE connection and process management inconsistencies**: Events that should be notified via SSE aren't shared between processes
|
||||
|
||||
Therefore, **development mode is sufficient for UI verification and minor changes**, but **production build startup is essential for comprehensive testing of session process management and SSE integration**.
|
||||
|
||||
### Production Mode (Recommended)
|
||||
|
||||
For comprehensive functionality testing, build and run in production mode:
|
||||
Build and run in production mode:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
@@ -91,7 +85,12 @@ pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
The built application is output to the `dist/` directory and started with `pnpm start`.
|
||||
The built application is output to the `dist/` directory:
|
||||
- `dist/static/` - Frontend static files (built by Vite)
|
||||
- `dist/main.js` - Backend server (built by esbuild)
|
||||
- `dist/index.js` - CLI entry point
|
||||
|
||||
The production server serves static files and handles API requests on a single port (3000 by default).
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
@@ -164,25 +163,32 @@ When the `vrt` label is added to a PR, CI automatically captures and commits sna
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/[[...route]]/ # Hono API Routes
|
||||
│ ├── components/ # Page-specific components
|
||||
│ ├── projects/ # Project-related pages
|
||||
│ └── ...
|
||||
├── components/ # Shared UI components
|
||||
├── lib/ # Frontend common logic
|
||||
│ ├── api/ # API client (Hono RPC)
|
||||
│ ├── sse/ # SSE connection management
|
||||
├── routes/ # TanStack Router routes
|
||||
│ ├── __root.tsx # Root route with providers
|
||||
│ ├── index.tsx # Home route
|
||||
│ └── projects/ # Project-related routes
|
||||
├── app/ # Shared components and hooks (legacy directory name)
|
||||
│ ├── components/ # Shared components
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ └── projects/ # Project-related page components
|
||||
├── components/ # UI components library
|
||||
│ └── ui/ # shadcn/ui components
|
||||
├── lib/ # Frontend common logic
|
||||
│ ├── api/ # API client (Hono RPC)
|
||||
│ ├── sse/ # SSE connection management
|
||||
│ └── conversation-schema/ # Zod schemas for conversation logs
|
||||
├── server/ # Backend implementation
|
||||
│ ├── core/ # Core domain logic (Effect-TS)
|
||||
│ │ ├── claude-code/ # Claude Code integration
|
||||
│ │ ├── events/ # SSE event management
|
||||
│ │ ├── session/ # Session management
|
||||
├── server/ # Backend implementation
|
||||
│ ├── core/ # Core domain logic (Effect-TS)
|
||||
│ │ ├── claude-code/ # Claude Code integration
|
||||
│ │ ├── events/ # SSE event management
|
||||
│ │ ├── session/ # Session management
|
||||
│ │ └── ...
|
||||
│ ├── hono/ # Hono application
|
||||
│ └── lib/ # Backend common utilities
|
||||
└── testing/ # Test helpers and mocks
|
||||
│ ├── hono/ # Hono application
|
||||
│ │ ├── app.ts # Hono app instance
|
||||
│ │ └── route.ts # API routes definition
|
||||
│ ├── lib/ # Backend common utilities
|
||||
│ └── main.ts # Server entry point
|
||||
└── testing/ # Test helpers and mocks
|
||||
```
|
||||
|
||||
## Development Tips
|
||||
|
||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web Viewer for Claude Code history"
|
||||
/>
|
||||
<title>Claude Code Viewer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
typescript: {
|
||||
ignoreBuildErrors: true, // typechecking should be separeted by build
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
19
package.json
19
package.json
@@ -23,9 +23,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "run-p 'dev:*'",
|
||||
"dev:next": "next dev --turbopack",
|
||||
"dev:frontend": "vite",
|
||||
"dev:backend": "NODE_ENV=development tsx watch src/server/main.ts --env-file-if-exists=.env.local",
|
||||
"start": "node dist/index.js",
|
||||
"build": "./scripts/build.sh",
|
||||
"build:frontend": "vite build",
|
||||
"build:backend": "esbuild src/server/main.ts --format=esm --bundle --packages=external --sourcemap --platform=node --outfile=dist/main.js",
|
||||
"lint": "run-s 'lint:*'",
|
||||
"lint:biome-format": "biome format .",
|
||||
"lint:biome-lint": "biome check .",
|
||||
@@ -46,6 +49,7 @@
|
||||
"@anthropic-ai/claude-code": "^2.0.24",
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-node": "^0.98.4",
|
||||
"@hono/node-server": "^1.19.5",
|
||||
"@hono/zod-validator": "^0.7.4",
|
||||
"@lingui/core": "^5.5.1",
|
||||
"@lingui/react": "^5.5.1",
|
||||
@@ -58,7 +62,11 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tanstack/react-devtools": "^0.7.8",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-router": "^1.133.27",
|
||||
"@tanstack/react-router-devtools": "^1.133.27",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -67,8 +75,6 @@
|
||||
"hono": "^4.10.1",
|
||||
"jotai": "^2.15.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"next": "15.5.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"parse-git-diff": "^0.0.19",
|
||||
"prexit": "^2.3.0",
|
||||
"react": "^19.2.0",
|
||||
@@ -88,19 +94,26 @@
|
||||
"@lingui/conf": "^5.5.1",
|
||||
"@lingui/format-json": "^5.5.1",
|
||||
"@lingui/loader": "^5.5.1",
|
||||
"@lingui/vite-plugin": "^5.5.1",
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@tanstack/router-plugin": "^1.133.27",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"esbuild": "^0.25.11",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"playwright": "^1.56.1",
|
||||
"release-it": "^19.0.5",
|
||||
"release-it-pnpm": "^4.6.6",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"tsx": "^4.20.6",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.12",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d"
|
||||
|
||||
1573
pnpm-lock.yaml
generated
1573
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
postcss.config.json
Normal file
3
postcss.config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["@tailwindcss/postcss"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
10
src/@types/env.d.ts
vendored
Normal file
10
src/@types/env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "process" {
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
DEV_BE_PORT?: string;
|
||||
PORT?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { NodeContext } from "@effect/platform-node";
|
||||
import { Effect } from "effect";
|
||||
import { handle } from "hono/vercel";
|
||||
import { ClaudeCodeController } from "../../../server/core/claude-code/presentation/ClaudeCodeController";
|
||||
import { ClaudeCodePermissionController } from "../../../server/core/claude-code/presentation/ClaudeCodePermissionController";
|
||||
import { ClaudeCodeSessionProcessController } from "../../../server/core/claude-code/presentation/ClaudeCodeSessionProcessController";
|
||||
import { ClaudeCodeLifeCycleService } from "../../../server/core/claude-code/services/ClaudeCodeLifeCycleService";
|
||||
import { ClaudeCodePermissionService } from "../../../server/core/claude-code/services/ClaudeCodePermissionService";
|
||||
import { ClaudeCodeService } from "../../../server/core/claude-code/services/ClaudeCodeService";
|
||||
import { ClaudeCodeSessionProcessService } from "../../../server/core/claude-code/services/ClaudeCodeSessionProcessService";
|
||||
import { SSEController } from "../../../server/core/events/presentation/SSEController";
|
||||
import { FileWatcherService } from "../../../server/core/events/services/fileWatcher";
|
||||
import { FileSystemController } from "../../../server/core/file-system/presentation/FileSystemController";
|
||||
import { GitController } from "../../../server/core/git/presentation/GitController";
|
||||
import { GitService } from "../../../server/core/git/services/GitService";
|
||||
import { ProjectRepository } from "../../../server/core/project/infrastructure/ProjectRepository";
|
||||
import { ProjectController } from "../../../server/core/project/presentation/ProjectController";
|
||||
import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService";
|
||||
import { SchedulerConfigBaseDir } from "../../../server/core/scheduler/config";
|
||||
import { SchedulerService } from "../../../server/core/scheduler/domain/Scheduler";
|
||||
import { SchedulerController } from "../../../server/core/scheduler/presentation/SchedulerController";
|
||||
import { SessionRepository } from "../../../server/core/session/infrastructure/SessionRepository";
|
||||
import { VirtualConversationDatabase } from "../../../server/core/session/infrastructure/VirtualConversationDatabase";
|
||||
import { SessionController } from "../../../server/core/session/presentation/SessionController";
|
||||
import { SessionMetaService } from "../../../server/core/session/services/SessionMetaService";
|
||||
import { honoApp } from "../../../server/hono/app";
|
||||
import { InitializeService } from "../../../server/hono/initialize";
|
||||
import { routes } from "../../../server/hono/route";
|
||||
import { platformLayer } from "../../../server/lib/effect/layers";
|
||||
|
||||
const program = routes(honoApp);
|
||||
|
||||
await Effect.runPromise(
|
||||
program
|
||||
// 依存の浅い順にコンテナに pipe する必要がある
|
||||
.pipe(
|
||||
/** Presentation */
|
||||
Effect.provide(ProjectController.Live),
|
||||
Effect.provide(SessionController.Live),
|
||||
Effect.provide(GitController.Live),
|
||||
Effect.provide(ClaudeCodeController.Live),
|
||||
Effect.provide(ClaudeCodeSessionProcessController.Live),
|
||||
Effect.provide(ClaudeCodePermissionController.Live),
|
||||
Effect.provide(FileSystemController.Live),
|
||||
Effect.provide(SSEController.Live),
|
||||
Effect.provide(SchedulerController.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Application */
|
||||
Effect.provide(InitializeService.Live),
|
||||
Effect.provide(FileWatcherService.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Domain */
|
||||
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
||||
Effect.provide(ClaudeCodePermissionService.Live),
|
||||
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
||||
Effect.provide(ClaudeCodeService.Live),
|
||||
Effect.provide(GitService.Live),
|
||||
Effect.provide(SchedulerService.Live),
|
||||
Effect.provide(SchedulerConfigBaseDir.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Infrastructure */
|
||||
Effect.provide(ProjectRepository.Live),
|
||||
Effect.provide(SessionRepository.Live),
|
||||
Effect.provide(ProjectMetaService.Live),
|
||||
Effect.provide(SessionMetaService.Live),
|
||||
Effect.provide(VirtualConversationDatabase.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Platform */
|
||||
Effect.provide(platformLayer),
|
||||
Effect.provide(NodeContext.layer),
|
||||
),
|
||||
);
|
||||
|
||||
export const GET = handle(honoApp);
|
||||
export const POST = handle(honoApp);
|
||||
export const PUT = handle(honoApp);
|
||||
export const PATCH = handle(honoApp);
|
||||
export const DELETE = handle(honoApp);
|
||||
@@ -1,6 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import type { FC } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
@@ -19,7 +16,7 @@ export const MarkdownContent: FC<MarkdownContentProps> = ({
|
||||
content,
|
||||
className = "",
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const resolvedTheme = "light" as "light" | "dark"; // TODO: 設定から取り出す
|
||||
const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, Home, RefreshCw } from "lucide-react";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSetAtom } from "jotai";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useSetAtom } from "jotai";
|
||||
import { type FC, type PropsWithChildren, useEffect } from "react";
|
||||
import type { PublicSessionProcess } from "../../types/session-process";
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, Home, RefreshCw } from "lucide-react";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertCircle className="size-6 text-destructive" />
|
||||
<div>
|
||||
<CardTitle>Something went wrong</CardTitle>
|
||||
<CardDescription>
|
||||
An unexpected error occurred in the application
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle />
|
||||
<AlertTitle>Error Details</AlertTitle>
|
||||
<AlertDescription>
|
||||
<code className="text-xs">{error.message}</code>
|
||||
{error.digest && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Error ID: {error.digest}
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={reset} variant="default">
|
||||
<RefreshCw />
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Home />
|
||||
Go to Home
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { honoClient } from "../lib/api/client";
|
||||
import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper";
|
||||
import { configQuery } from "../lib/api/queries";
|
||||
import { LinguiServerProvider } from "../lib/i18n/LinguiServerProvider";
|
||||
import { SSEProvider } from "../lib/sse/components/SSEProvider";
|
||||
import { getUserConfigOnServerComponent } from "../server/lib/config/getUserConfigOnServerComponent";
|
||||
import { RootErrorBoundary } from "./components/RootErrorBoundary";
|
||||
import { SSEEventListeners } from "./components/SSEEventListeners";
|
||||
import { SyncSessionProcess } from "./components/SyncSessionProcess";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const fetchCache = "force-no-store";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: "Claude Code Viewer",
|
||||
description: "Web Viewer for Claude Code history",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const userConfig = await getUserConfigOnServerComponent();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: configQuery.queryKey,
|
||||
queryFn: configQuery.queryFn,
|
||||
});
|
||||
|
||||
const initSessionProcesses = await honoClient.api.cc["session-processes"]
|
||||
.$get({})
|
||||
.then((response) => response.json());
|
||||
|
||||
return (
|
||||
<html lang={userConfig.locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<LinguiServerProvider locale={userConfig.locale}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<RootErrorBoundary>
|
||||
<QueryClientProviderWrapper>
|
||||
<SSEProvider>
|
||||
<SSEEventListeners>
|
||||
<SyncSessionProcess
|
||||
initProcesses={initSessionProcesses.processes}
|
||||
>
|
||||
{children}
|
||||
</SyncSessionProcess>
|
||||
</SSEEventListeners>
|
||||
</SSEProvider>
|
||||
</QueryClientProviderWrapper>
|
||||
</RootErrorBoundary>
|
||||
<Toaster position="top-right" />
|
||||
</ThemeProvider>
|
||||
</LinguiServerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { FileQuestion, Home } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileQuestion className="size-6 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle>Page Not Found</CardTitle>
|
||||
<CardDescription>
|
||||
The page you are looking for does not exist
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="default">
|
||||
<Link href="/">
|
||||
<Home />
|
||||
Go to Home
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
redirect("/projects");
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { honoClient } from "../../../../../lib/api/client";
|
||||
|
||||
export const useCreateSessionProcessMutation = (
|
||||
projectId: string,
|
||||
onSuccess?: () => void,
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (options: {
|
||||
@@ -36,9 +36,13 @@ export const useCreateSessionProcessMutation = (
|
||||
},
|
||||
onSuccess: async (response) => {
|
||||
onSuccess?.();
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.sessionProcess.sessionId}`,
|
||||
);
|
||||
navigate({
|
||||
to: "/projects/$projectId/sessions/$sessionId",
|
||||
params: {
|
||||
projectId: projectId,
|
||||
sessionId: response.sessionProcess.sessionId,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { AlertCircle, ArrowLeft, RefreshCw } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function ProjectErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertCircle className="size-6 text-destructive" />
|
||||
<div>
|
||||
<CardTitle>
|
||||
<Trans
|
||||
id="project.error.title"
|
||||
message="Failed to load project"
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans
|
||||
id="project.error.description"
|
||||
message="We encountered an error while loading this project"
|
||||
/>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle />
|
||||
<AlertTitle>
|
||||
<Trans id="project.error.details_title" message="Error Details" />
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<code className="text-xs">{error.message}</code>
|
||||
{error.digest && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
<Trans id="project.error.error_id" message="Error ID:" />{" "}
|
||||
{error.digest}
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={reset} variant="default">
|
||||
<RefreshCw />
|
||||
<Trans id="project.error.try_again" message="Try Again" />
|
||||
</Button>
|
||||
<Button onClick={() => router.push("/projects")} variant="outline">
|
||||
<ArrowLeft />
|
||||
<Trans
|
||||
id="project.error.back_to_projects"
|
||||
message="Back to Projects"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { redirect } from "next/navigation";
|
||||
import { latestSessionQuery } from "../../../../lib/api/queries";
|
||||
import { initializeI18n } from "../../../../lib/i18n/initializeI18n";
|
||||
|
||||
interface LatestSessionPageProps {
|
||||
params: Promise<{ projectId: string }>;
|
||||
}
|
||||
|
||||
export default async function LatestSessionPage({
|
||||
params,
|
||||
}: LatestSessionPageProps) {
|
||||
await initializeI18n();
|
||||
|
||||
const { projectId } = await params;
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const { latestSession } = await queryClient.fetchQuery(
|
||||
latestSessionQuery(projectId),
|
||||
);
|
||||
|
||||
if (!latestSession) {
|
||||
redirect(`/projects`);
|
||||
}
|
||||
|
||||
redirect(`/projects/${projectId}/sessions/${latestSession.id}`);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { FolderSearch, Home } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { initializeI18n } from "../../../lib/i18n/initializeI18n";
|
||||
|
||||
export default async function ProjectNotFoundPage() {
|
||||
await initializeI18n();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderSearch className="size-6 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle>
|
||||
<Trans
|
||||
id="project.not_found.title"
|
||||
message="Project Not Found"
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans
|
||||
id="project.not_found.description"
|
||||
message="The project you are looking for does not exist or has been removed"
|
||||
/>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="default">
|
||||
<Link href="/projects">
|
||||
<Home />
|
||||
<Trans
|
||||
id="project.not_found.back_to_projects"
|
||||
message="Back to Projects"
|
||||
/>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -96,7 +94,7 @@ export const SessionPageContent: FC<{
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen max-h-screen overflow-hidden">
|
||||
<SessionSidebar
|
||||
currentSessionId={sessionId}
|
||||
projectId={projectId}
|
||||
@@ -275,6 +273,6 @@ export const SessionPageContent: FC<{
|
||||
isOpen={isDialogOpen}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { ChevronDown, Eye, Lightbulb, Wrench } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import type { FC } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import {
|
||||
@@ -42,7 +38,7 @@ export const AssistantConversationContent: FC<{
|
||||
getSidechainConversationByPrompt,
|
||||
getSidechainConversations,
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const resolvedTheme = "light" as "light" | "dark"; // TODO: 設定から取り出す
|
||||
const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight;
|
||||
if (content.type === "text") {
|
||||
return (
|
||||
@@ -195,7 +191,7 @@ export const AssistantConversationContent: FC<{
|
||||
toolResult.content.map((item) => {
|
||||
if (item.type === "image") {
|
||||
return (
|
||||
<Image
|
||||
<img
|
||||
key={item.source.data}
|
||||
src={`data:${item.source.media_type};base64,${item.source.data}`}
|
||||
alt="Tool Result"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { AlertTriangle, ChevronDown, ExternalLink } from "lucide-react";
|
||||
import { type FC, useMemo } from "react";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { AlertCircle, Image as ImageIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import type { FC } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -54,7 +53,7 @@ export const UserConversationContent: FC<{
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-lg border overflow-hidden bg-background">
|
||||
<Image
|
||||
<img
|
||||
src={`data:${content.source.media_type};base64,${content.source.data}`}
|
||||
alt="User uploaded content"
|
||||
className="max-w-full h-auto max-h-96 object-contain"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { Eye, MessageSquare } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import {
|
||||
ChevronDown,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronRightIcon, CopyIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
InfoIcon,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type FC, Suspense, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { NotificationSettings } from "@/components/NotificationSettings";
|
||||
@@ -192,7 +190,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/projects"
|
||||
to="/projects"
|
||||
className="w-12 h-12 flex items-center justify-center border-b border-sidebar-border hover:bg-sidebar-accent transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 text-sidebar-foreground/70" />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import { EditIcon, PlusIcon, RefreshCwIcon, TrashIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CalendarClockIcon,
|
||||
MessageSquareIcon,
|
||||
PlugIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type FC, useMemo } from "react";
|
||||
import type { SidebarTab } from "@/components/GlobalSidebar";
|
||||
import { GlobalSidebar } from "@/components/GlobalSidebar";
|
||||
@@ -103,7 +101,7 @@ export const SessionSidebar: FC<{
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/projects"
|
||||
to="/projects"
|
||||
className="w-12 h-12 flex items-center justify-center hover:bg-sidebar-accent transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 text-sidebar-foreground/70" />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { MessageSquareIcon, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -116,7 +114,8 @@ export const SessionsTab: FC<{
|
||||
return (
|
||||
<Link
|
||||
key={session.id}
|
||||
href={`/projects/${projectId}/sessions/${session.id}`}
|
||||
to={"/projects/$projectId/sessions/$sessionId"}
|
||||
params={{ projectId, sessionId: session.id }}
|
||||
className={cn(
|
||||
"block rounded-lg p-2.5 transition-all duration-200 hover:bg-blue-50/60 dark:hover:bg-blue-950/40 hover:border-blue-300/60 dark:hover:border-blue-700/60 hover:shadow-sm border border-sidebar-border/40 bg-sidebar/30",
|
||||
isActive &&
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, ArrowLeft, RefreshCw } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function SessionErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ projectId: string }>();
|
||||
const projectId = params.projectId;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertCircle className="size-6 text-destructive" />
|
||||
<div>
|
||||
<CardTitle>Failed to load session</CardTitle>
|
||||
<CardDescription>
|
||||
We encountered an error while loading this conversation session
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle />
|
||||
<AlertTitle>Error Details</AlertTitle>
|
||||
<AlertDescription>
|
||||
<code className="text-xs">{error.message}</code>
|
||||
{error.digest && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Error ID: {error.digest}
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={reset} variant="default">
|
||||
<RefreshCw />
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => router.push(`/projects/${projectId}/latest`)}
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back to Project
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
interface SessionLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const SessionLayout: FC<SessionLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen overflow-hidden">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionLayout;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { MessageCircleOff } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function SessionNotFoundPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<MessageCircleOff className="size-6 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle>Session Not Found</CardTitle>
|
||||
<CardDescription>
|
||||
The conversation session you are looking for does not exist or
|
||||
has been removed
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="default">
|
||||
<Link href="/projects">
|
||||
<MessageCircleOff />
|
||||
Back to Projects
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
projectDetailQuery,
|
||||
sessionDetailQuery,
|
||||
} from "../../../../../lib/api/queries";
|
||||
import { initializeI18n } from "../../../../../lib/i18n/initializeI18n";
|
||||
import { SessionPageContent } from "./components/SessionPageContent";
|
||||
|
||||
type PageParams = {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<PageParams>;
|
||||
}): Promise<Metadata> {
|
||||
const { projectId, sessionId } = await params;
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
...sessionDetailQuery(projectId, sessionId),
|
||||
});
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: projectDetailQuery(projectId).queryKey,
|
||||
queryFn: projectDetailQuery(projectId).queryFn,
|
||||
});
|
||||
|
||||
return {
|
||||
title: `Session: ${sessionId.slice(0, 8)}...`,
|
||||
description: `View conversation session ${projectId}/${sessionId}`,
|
||||
};
|
||||
}
|
||||
|
||||
interface SessionPageProps {
|
||||
params: Promise<PageParams>;
|
||||
}
|
||||
|
||||
export default async function SessionPage({ params }: SessionPageProps) {
|
||||
const { projectId, sessionId } = await params;
|
||||
await initializeI18n();
|
||||
|
||||
return <SessionPageContent projectId={projectId} sessionId={sessionId} />;
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FC, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -22,7 +20,7 @@ import { DirectoryPicker } from "./DirectoryPicker";
|
||||
export const CreateProjectDialog: FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPath, setSelectedPath] = useState<string>("");
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -40,7 +38,13 @@ export const CreateProjectDialog: FC = () => {
|
||||
onSuccess: (result) => {
|
||||
toast.success("Project created successfully");
|
||||
setOpen(false);
|
||||
router.push(`/projects/${result.projectId}/sessions/${result.sessionId}`);
|
||||
navigate({
|
||||
to: "/projects/$projectId/sessions/$sessionId",
|
||||
params: {
|
||||
projectId: result.projectId,
|
||||
sessionId: result.sessionId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight, Folder } from "lucide-react";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { FolderIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -74,7 +72,10 @@ export const ProjectList: FC = () => {
|
||||
</CardContent>
|
||||
<CardContent className="pt-0">
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/projects/${project.id}/latest`}>
|
||||
<Link
|
||||
to={"/projects/$projectId/latest"}
|
||||
params={{ projectId: project.id }}
|
||||
>
|
||||
<Trans
|
||||
id="project_list.view_conversations"
|
||||
message="View Conversations"
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { HistoryIcon } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { type FC, Suspense } from "react";
|
||||
import { GlobalSidebar } from "@/components/GlobalSidebar";
|
||||
import { initializeI18n } from "../../lib/i18n/initializeI18n";
|
||||
import { CreateProjectDialog } from "./components/CreateProjectDialog";
|
||||
import { ProjectList } from "./components/ProjectList";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const fetchCache = "force-no-store";
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
await initializeI18n();
|
||||
|
||||
export const ProjectsPage: FC = () => {
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen overflow-hidden">
|
||||
<GlobalSidebar />
|
||||
@@ -58,4 +52,4 @@ export default async function ProjectsPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { InfoIcon, SettingsIcon } from "lucide-react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useAtom } from "jotai";
|
||||
import { type FC, useCallback, useId } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronRight, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useTheme } from "next-themes";
|
||||
import { type FC, useId } from "react";
|
||||
import { useConfig } from "@/app/hooks/useConfig";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -35,7 +32,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
const themeId = useId();
|
||||
const { config, updateConfig } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const theme = "system"; // TODO: 設定から取り出す
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const handleHideNoUserMessageChange = async () => {
|
||||
@@ -301,7 +298,12 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
<Trans id="settings.theme" message="Theme" />
|
||||
</label>
|
||||
)}
|
||||
<Select value={theme || "system"} onValueChange={setTheme}>
|
||||
<Select
|
||||
value={theme || "system"}
|
||||
onValueChange={() => {
|
||||
// TODO: 設定を更新する
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={themeId} className="w-full">
|
||||
<SelectValue placeholder={i18n._("Select theme")} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle2, ChevronDown, ChevronRight, XCircle } from "lucide-react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { InlineCompletion } from "@/app/projects/[projectId]/components/chatForm/InlineCompletion";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import type * as React from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import type * as React from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
const theme = "system"; // TODO: 設定から取り出す
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import type * as React from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useServerEventListener } from "@/lib/sse/hook/useServerEventListener";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
NewSchedulerJob,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
isServer,
|
||||
QueryClient,
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { hc } from "hono/client";
|
||||
import type { RouteType } from "../../server/hono/route";
|
||||
|
||||
export const honoClient = hc<RouteType>(
|
||||
typeof window === "undefined"
|
||||
? // biome-ignore lint/complexity/useLiteralKeys: allow here
|
||||
// biome-ignore lint/style/noProcessEnv: allow here
|
||||
`http://localhost:${process.env["PORT"]}/`
|
||||
: "/",
|
||||
);
|
||||
export const honoClient = hc<RouteType>("/");
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { type Messages, setupI18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { type FC, type PropsWithChildren, useState } from "react";
|
||||
|
||||
export const LinguiClientProvider: FC<
|
||||
PropsWithChildren<{
|
||||
initialLocale: string;
|
||||
initialMessages: Messages;
|
||||
}>
|
||||
> = ({ children, initialLocale, initialMessages }) => {
|
||||
const [i18n] = useState(() => {
|
||||
return setupI18n({
|
||||
locale: initialLocale,
|
||||
messages: { [initialLocale]: initialMessages },
|
||||
});
|
||||
});
|
||||
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
|
||||
};
|
||||
14
src/lib/i18n/LinguiProvider.tsx
Normal file
14
src/lib/i18n/LinguiProvider.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { i18nMessages } from ".";
|
||||
|
||||
for (const { locale, messages } of i18nMessages) {
|
||||
i18n.load(locale, messages);
|
||||
}
|
||||
|
||||
i18n.activate("en");
|
||||
|
||||
export const LinguiClientProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { setI18n } from "@lingui/react/server";
|
||||
import { getI18nInstance } from "./index";
|
||||
import { LinguiClientProvider } from "./LinguiClientProvider";
|
||||
import type { SupportedLocale } from "./schema";
|
||||
|
||||
export async function LinguiServerProvider(props: {
|
||||
locale: SupportedLocale;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, locale } = props;
|
||||
|
||||
const i18n = getI18nInstance(locale);
|
||||
setI18n(i18n);
|
||||
|
||||
return (
|
||||
<LinguiClientProvider
|
||||
initialLocale={locale}
|
||||
initialMessages={i18n.messages}
|
||||
>
|
||||
{children}
|
||||
</LinguiClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +1,20 @@
|
||||
import "server-only";
|
||||
|
||||
import { type I18n, type Messages, setupI18n } from "@lingui/core";
|
||||
import type { Messages } from "@lingui/core";
|
||||
import { messages as enMessages } from "./locales/en/messages";
|
||||
import { messages as jaMessages } from "./locales/ja/messages";
|
||||
import type { SupportedLocale } from "./schema";
|
||||
|
||||
const locales: SupportedLocale[] = ["ja", "en"];
|
||||
export const locales: SupportedLocale[] = ["ja", "en"];
|
||||
|
||||
async function loadCatalog(locale: SupportedLocale): Promise<{
|
||||
[k: string]: Messages;
|
||||
}> {
|
||||
const { messages } = await import(`./locales/${locale}/messages`);
|
||||
return {
|
||||
[locale]: messages,
|
||||
};
|
||||
}
|
||||
const catalogs = await Promise.all(locales.map(loadCatalog));
|
||||
|
||||
export const allMessages = catalogs.reduce((acc, oneCatalog) => {
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: size is small
|
||||
return { ...acc, ...oneCatalog };
|
||||
}, {});
|
||||
|
||||
type AllI18nInstances = { [K in SupportedLocale]: I18n };
|
||||
|
||||
export const allI18nInstances = locales.reduce(
|
||||
(acc: Partial<AllI18nInstances>, locale) => {
|
||||
const messages = allMessages[locale] ?? {};
|
||||
const i18n = setupI18n({
|
||||
locale,
|
||||
messages: { [locale]: messages },
|
||||
});
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: size is small
|
||||
return { ...acc, [locale]: i18n };
|
||||
export const i18nMessages = [
|
||||
{
|
||||
locale: "ja",
|
||||
messages: jaMessages,
|
||||
},
|
||||
{},
|
||||
) as AllI18nInstances;
|
||||
|
||||
export const getI18nInstance = (locale: SupportedLocale): I18n => {
|
||||
if (!allI18nInstances[locale]) {
|
||||
console.warn(`No i18n instance found for locale "${locale}"`);
|
||||
}
|
||||
|
||||
const instance = allI18nInstances[locale] ?? allI18nInstances.en;
|
||||
|
||||
if (instance === undefined) {
|
||||
throw new Error(`No i18n instance found for locale "${locale}"`);
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
{
|
||||
locale: "en",
|
||||
messages: enMessages,
|
||||
},
|
||||
] as const satisfies Array<{
|
||||
locale: SupportedLocale;
|
||||
messages: Messages;
|
||||
}>;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { setI18n } from "@lingui/react/server";
|
||||
import { getUserConfigOnServerComponent } from "../../server/lib/config/getUserConfigOnServerComponent";
|
||||
import { getI18nInstance } from ".";
|
||||
|
||||
export const initializeI18n = async () => {
|
||||
const userConfig = await getUserConfigOnServerComponent();
|
||||
const i18n = getI18nInstance(userConfig.locale);
|
||||
setI18n(i18n);
|
||||
};
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import type { SSEEvent } from "../../types/sse";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { ServerEventsProvider } from "./ServerEventsProvider";
|
||||
|
||||
|
||||
32
src/main.tsx
Normal file
32
src/main.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {},
|
||||
defaultPreload: "intent",
|
||||
scrollRestoration: true,
|
||||
defaultStructuralSharing: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
});
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("app");
|
||||
if (rootElement && !rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
129
src/routeTree.gen.ts
Normal file
129
src/routeTree.gen.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as ProjectsProjectIdLatestIndexRouteImport } from "./routes/projects/$projectId/latest/index";
|
||||
import { Route as ProjectsProjectIdSessionsSessionIdIndexRouteImport } from "./routes/projects/$projectId/sessions/$sessionId/index";
|
||||
import { Route as ProjectsIndexRouteImport } from "./routes/projects/index";
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ProjectsIndexRoute = ProjectsIndexRouteImport.update({
|
||||
id: "/projects/",
|
||||
path: "/projects/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ProjectsProjectIdLatestIndexRoute =
|
||||
ProjectsProjectIdLatestIndexRouteImport.update({
|
||||
id: "/projects/$projectId/latest/",
|
||||
path: "/projects/$projectId/latest/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ProjectsProjectIdSessionsSessionIdIndexRoute =
|
||||
ProjectsProjectIdSessionsSessionIdIndexRouteImport.update({
|
||||
id: "/projects/$projectId/sessions/$sessionId/",
|
||||
path: "/projects/$projectId/sessions/$sessionId/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/projects": typeof ProjectsIndexRoute;
|
||||
"/projects/$projectId/latest": typeof ProjectsProjectIdLatestIndexRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ProjectsProjectIdSessionsSessionIdIndexRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/projects": typeof ProjectsIndexRoute;
|
||||
"/projects/$projectId/latest": typeof ProjectsProjectIdLatestIndexRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ProjectsProjectIdSessionsSessionIdIndexRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/projects/": typeof ProjectsIndexRoute;
|
||||
"/projects/$projectId/latest/": typeof ProjectsProjectIdLatestIndexRoute;
|
||||
"/projects/$projectId/sessions/$sessionId/": typeof ProjectsProjectIdSessionsSessionIdIndexRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/projects"
|
||||
| "/projects/$projectId/latest"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| "/"
|
||||
| "/projects"
|
||||
| "/projects/$projectId/latest"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/projects/"
|
||||
| "/projects/$projectId/latest/"
|
||||
| "/projects/$projectId/sessions/$sessionId/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
ProjectsIndexRoute: typeof ProjectsIndexRoute;
|
||||
ProjectsProjectIdLatestIndexRoute: typeof ProjectsProjectIdLatestIndexRoute;
|
||||
ProjectsProjectIdSessionsSessionIdIndexRoute: typeof ProjectsProjectIdSessionsSessionIdIndexRoute;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/projects/": {
|
||||
id: "/projects/";
|
||||
path: "/projects";
|
||||
fullPath: "/projects";
|
||||
preLoaderRoute: typeof ProjectsIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/projects/$projectId/latest/": {
|
||||
id: "/projects/$projectId/latest/";
|
||||
path: "/projects/$projectId/latest";
|
||||
fullPath: "/projects/$projectId/latest";
|
||||
preLoaderRoute: typeof ProjectsProjectIdLatestIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/projects/$projectId/sessions/$sessionId/": {
|
||||
id: "/projects/$projectId/sessions/$sessionId/";
|
||||
path: "/projects/$projectId/sessions/$sessionId";
|
||||
fullPath: "/projects/$projectId/sessions/$sessionId";
|
||||
preLoaderRoute: typeof ProjectsProjectIdSessionsSessionIdIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ProjectsIndexRoute: ProjectsIndexRoute,
|
||||
ProjectsProjectIdLatestIndexRoute: ProjectsProjectIdLatestIndexRoute,
|
||||
ProjectsProjectIdSessionsSessionIdIndexRoute:
|
||||
ProjectsProjectIdSessionsSessionIdIndexRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
37
src/routes/__root.tsx
Normal file
37
src/routes/__root.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { RootErrorBoundary } from "../app/components/RootErrorBoundary";
|
||||
import { SSEEventListeners } from "../app/components/SSEEventListeners";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper";
|
||||
import { LinguiClientProvider } from "../lib/i18n/LinguiProvider";
|
||||
import { SSEProvider } from "../lib/sse/components/SSEProvider";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<RootErrorBoundary>
|
||||
<QueryClientProviderWrapper>
|
||||
<LinguiClientProvider>
|
||||
<SSEProvider>
|
||||
<SSEEventListeners>
|
||||
<Outlet />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SSEEventListeners>
|
||||
</SSEProvider>
|
||||
</LinguiClientProvider>
|
||||
</QueryClientProviderWrapper>
|
||||
<Toaster position="top-right" />
|
||||
</RootErrorBoundary>
|
||||
),
|
||||
});
|
||||
10
src/routes/index.tsx
Normal file
10
src/routes/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const router = useRouter();
|
||||
router.navigate({ to: "/projects" });
|
||||
}
|
||||
55
src/routes/projects/$projectId/latest/index.tsx
Normal file
55
src/routes/projects/$projectId/latest/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
createFileRoute,
|
||||
useLoaderData,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { honoClient } from "../../../../lib/api/client";
|
||||
|
||||
export const Route = createFileRoute("/projects/$projectId/latest/")({
|
||||
component: RouteComponent,
|
||||
loader: async ({ params }) => {
|
||||
const { projectId } = params;
|
||||
const response = await honoClient.api.projects[":projectId"][
|
||||
"latest-session"
|
||||
].$get({
|
||||
param: { projectId },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to fetch latest session: ${response.statusText}`,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
projectId,
|
||||
data: await response.json(),
|
||||
} as const;
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const router = useRouter();
|
||||
const loaderData = useLoaderData({ from: "/projects/$projectId/latest/" });
|
||||
|
||||
if (!loaderData.success) {
|
||||
return <div>{loaderData.message}</div>;
|
||||
}
|
||||
|
||||
const latestSession = loaderData.data.latestSession;
|
||||
if (latestSession === null) {
|
||||
router.navigate({ to: "/projects" });
|
||||
return null;
|
||||
}
|
||||
|
||||
router.navigate({
|
||||
to: "/projects/$projectId/sessions/$sessionId",
|
||||
params: {
|
||||
projectId: loaderData.projectId,
|
||||
sessionId: latestSession.id,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
18
src/routes/projects/$projectId/sessions/$sessionId/index.tsx
Normal file
18
src/routes/projects/$projectId/sessions/$sessionId/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SessionPageContent } from "../../../../../app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/projects/$projectId/sessions/$sessionId/",
|
||||
)({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const params = Route.useParams();
|
||||
return (
|
||||
<SessionPageContent
|
||||
projectId={params.projectId}
|
||||
sessionId={params.sessionId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
10
src/routes/projects/index.tsx
Normal file
10
src/routes/projects/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ProjectsPage } from "../../app/projects/page";
|
||||
|
||||
export const Route = createFileRoute("/projects/")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <ProjectsPage />;
|
||||
}
|
||||
@@ -7,6 +7,6 @@ export type HonoContext = {
|
||||
};
|
||||
};
|
||||
|
||||
export const honoApp = new Hono<HonoContext>().basePath("/api");
|
||||
export const honoApp = new Hono<HonoContext>();
|
||||
|
||||
export type HonoAppType = typeof honoApp;
|
||||
|
||||
@@ -91,13 +91,13 @@ export const routes = (app: HonoAppType) =>
|
||||
})
|
||||
|
||||
// routes
|
||||
.get("/config", async (c) => {
|
||||
.get("/api/config", async (c) => {
|
||||
return c.json({
|
||||
config: c.get("userConfig"),
|
||||
});
|
||||
})
|
||||
|
||||
.put("/config", zValidator("json", userConfigSchema), async (c) => {
|
||||
.put("/api/config", zValidator("json", userConfigSchema), async (c) => {
|
||||
const { ...config } = c.req.valid("json");
|
||||
|
||||
setCookie(c, "ccv-config", JSON.stringify(config));
|
||||
@@ -107,7 +107,7 @@ export const routes = (app: HonoAppType) =>
|
||||
});
|
||||
})
|
||||
|
||||
.get("/version", async (c) => {
|
||||
.get("/api/version", async (c) => {
|
||||
return c.json({
|
||||
version: packageJson.version,
|
||||
});
|
||||
@@ -117,7 +117,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* ProjectController Routes
|
||||
*/
|
||||
|
||||
.get("/projects", async (c) => {
|
||||
.get("/api/projects", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
projectController.getProjects(),
|
||||
@@ -126,7 +126,7 @@ export const routes = (app: HonoAppType) =>
|
||||
})
|
||||
|
||||
.get(
|
||||
"/projects/:projectId",
|
||||
"/api/projects/:projectId",
|
||||
zValidator("query", z.object({ cursor: z.string().optional() })),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -143,7 +143,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.post(
|
||||
"/projects",
|
||||
"/api/projects",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
@@ -163,7 +163,7 @@ export const routes = (app: HonoAppType) =>
|
||||
},
|
||||
)
|
||||
|
||||
.get("/projects/:projectId/latest-session", async (c) => {
|
||||
.get("/api/projects/:projectId/latest-session", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
projectController
|
||||
@@ -179,7 +179,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* SessionController Routes
|
||||
*/
|
||||
|
||||
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
||||
.get("/api/projects/:projectId/sessions/:sessionId", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
sessionController
|
||||
@@ -193,7 +193,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* GitController Routes
|
||||
*/
|
||||
|
||||
.get("/projects/:projectId/git/branches", async (c) => {
|
||||
.get("/api/projects/:projectId/git/branches", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
gitController
|
||||
@@ -205,7 +205,7 @@ export const routes = (app: HonoAppType) =>
|
||||
return response;
|
||||
})
|
||||
|
||||
.get("/projects/:projectId/git/commits", async (c) => {
|
||||
.get("/api/projects/:projectId/git/commits", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
gitController
|
||||
@@ -218,7 +218,7 @@ export const routes = (app: HonoAppType) =>
|
||||
})
|
||||
|
||||
.post(
|
||||
"/projects/:projectId/git/diff",
|
||||
"/api/projects/:projectId/git/diff",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
@@ -241,7 +241,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.post(
|
||||
"/projects/:projectId/git/commit",
|
||||
"/api/projects/:projectId/git/commit",
|
||||
zValidator("json", CommitRequestSchema),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -258,7 +258,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.post(
|
||||
"/projects/:projectId/git/push",
|
||||
"/api/projects/:projectId/git/push",
|
||||
zValidator("json", PushRequestSchema),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -275,7 +275,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.post(
|
||||
"/projects/:projectId/git/commit-and-push",
|
||||
"/api/projects/:projectId/git/commit-and-push",
|
||||
zValidator("json", CommitRequestSchema),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -295,7 +295,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* ClaudeCodeController Routes
|
||||
*/
|
||||
|
||||
.get("/projects/:projectId/claude-commands", async (c) => {
|
||||
.get("/api/projects/:projectId/claude-commands", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
claudeCodeController.getClaudeCommands({
|
||||
@@ -305,7 +305,7 @@ export const routes = (app: HonoAppType) =>
|
||||
return response;
|
||||
})
|
||||
|
||||
.get("/projects/:projectId/mcp/list", async (c) => {
|
||||
.get("/api/projects/:projectId/mcp/list", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
claudeCodeController
|
||||
@@ -317,7 +317,7 @@ export const routes = (app: HonoAppType) =>
|
||||
return response;
|
||||
})
|
||||
|
||||
.get("/cc/meta", async (c) => {
|
||||
.get("/api/cc/meta", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
claudeCodeController
|
||||
@@ -327,7 +327,7 @@ export const routes = (app: HonoAppType) =>
|
||||
return response;
|
||||
})
|
||||
|
||||
.get("/cc/features", async (c) => {
|
||||
.get("/api/cc/features", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
claudeCodeController
|
||||
@@ -341,7 +341,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* ClaudeCodeSessionProcessController Routes
|
||||
*/
|
||||
|
||||
.get("/cc/session-processes", async (c) => {
|
||||
.get("/api/cc/session-processes", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
claudeCodeSessionProcessController.getSessionProcesses(),
|
||||
@@ -351,7 +351,7 @@ export const routes = (app: HonoAppType) =>
|
||||
|
||||
// new or resume
|
||||
.post(
|
||||
"/cc/session-processes",
|
||||
"/api/cc/session-processes",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
@@ -373,7 +373,7 @@ export const routes = (app: HonoAppType) =>
|
||||
|
||||
// continue
|
||||
.post(
|
||||
"/cc/session-processes/:sessionProcessId/continue",
|
||||
"/api/cc/session-processes/:sessionProcessId/continue",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
@@ -397,7 +397,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.post(
|
||||
"/cc/session-processes/:sessionProcessId/abort",
|
||||
"/api/cc/session-processes/:sessionProcessId/abort",
|
||||
zValidator("json", z.object({ projectId: z.string() })),
|
||||
async (c) => {
|
||||
const { sessionProcessId } = c.req.param();
|
||||
@@ -413,7 +413,7 @@ export const routes = (app: HonoAppType) =>
|
||||
*/
|
||||
|
||||
.post(
|
||||
"/cc/permission-response",
|
||||
"/api/cc/permission-response",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
@@ -436,7 +436,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* SSEController Routes
|
||||
*/
|
||||
|
||||
.get("/sse", async (c) => {
|
||||
.get("/api/sse", async (c) => {
|
||||
return streamSSE(
|
||||
c,
|
||||
async (rawStream) => {
|
||||
@@ -456,7 +456,7 @@ export const routes = (app: HonoAppType) =>
|
||||
* SchedulerController Routes
|
||||
*/
|
||||
|
||||
.get("/scheduler/jobs", async (c) => {
|
||||
.get("/api/scheduler/jobs", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
schedulerController.getJobs().pipe(Effect.provide(runtime)),
|
||||
@@ -465,7 +465,7 @@ export const routes = (app: HonoAppType) =>
|
||||
})
|
||||
|
||||
.post(
|
||||
"/scheduler/jobs",
|
||||
"/api/scheduler/jobs",
|
||||
zValidator("json", newSchedulerJobSchema),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -481,7 +481,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.patch(
|
||||
"/scheduler/jobs/:id",
|
||||
"/api/scheduler/jobs/:id",
|
||||
zValidator("json", updateSchedulerJobSchema),
|
||||
async (c) => {
|
||||
const response = await effectToResponse(
|
||||
@@ -497,7 +497,7 @@ export const routes = (app: HonoAppType) =>
|
||||
},
|
||||
)
|
||||
|
||||
.delete("/scheduler/jobs/:id", async (c) => {
|
||||
.delete("/api/scheduler/jobs/:id", async (c) => {
|
||||
const response = await effectToResponse(
|
||||
c,
|
||||
schedulerController
|
||||
@@ -514,12 +514,12 @@ export const routes = (app: HonoAppType) =>
|
||||
*/
|
||||
|
||||
.get(
|
||||
"/fs/file-completion",
|
||||
"/api/fs/file-completion",
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
basePath: z.string().optional().default("/"),
|
||||
basePath: z.string().optional().default("/api/"),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -535,7 +535,7 @@ export const routes = (app: HonoAppType) =>
|
||||
)
|
||||
|
||||
.get(
|
||||
"/fs/directory-browser",
|
||||
"/api/fs/directory-browser",
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { parseUserConfig } from "./parseUserConfig";
|
||||
|
||||
export const getUserConfigOnServerComponent = async () => {
|
||||
const cookie = await cookies();
|
||||
const userConfigJson = cookie.get("ccv-config")?.value;
|
||||
return parseUserConfig(userConfigJson);
|
||||
};
|
||||
117
src/server/main.ts
Normal file
117
src/server/main.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { NodeContext } from "@effect/platform-node";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { Effect } from "effect";
|
||||
import { ClaudeCodeController } from "./core/claude-code/presentation/ClaudeCodeController";
|
||||
import { ClaudeCodePermissionController } from "./core/claude-code/presentation/ClaudeCodePermissionController";
|
||||
import { ClaudeCodeSessionProcessController } from "./core/claude-code/presentation/ClaudeCodeSessionProcessController";
|
||||
import { ClaudeCodeLifeCycleService } from "./core/claude-code/services/ClaudeCodeLifeCycleService";
|
||||
import { ClaudeCodePermissionService } from "./core/claude-code/services/ClaudeCodePermissionService";
|
||||
import { ClaudeCodeService } from "./core/claude-code/services/ClaudeCodeService";
|
||||
import { ClaudeCodeSessionProcessService } from "./core/claude-code/services/ClaudeCodeSessionProcessService";
|
||||
import { SSEController } from "./core/events/presentation/SSEController";
|
||||
import { FileWatcherService } from "./core/events/services/fileWatcher";
|
||||
import { FileSystemController } from "./core/file-system/presentation/FileSystemController";
|
||||
import { GitController } from "./core/git/presentation/GitController";
|
||||
import { GitService } from "./core/git/services/GitService";
|
||||
import { ProjectRepository } from "./core/project/infrastructure/ProjectRepository";
|
||||
import { ProjectController } from "./core/project/presentation/ProjectController";
|
||||
import { ProjectMetaService } from "./core/project/services/ProjectMetaService";
|
||||
import { SchedulerConfigBaseDir } from "./core/scheduler/config";
|
||||
import { SchedulerService } from "./core/scheduler/domain/Scheduler";
|
||||
import { SchedulerController } from "./core/scheduler/presentation/SchedulerController";
|
||||
import { SessionRepository } from "./core/session/infrastructure/SessionRepository";
|
||||
import { VirtualConversationDatabase } from "./core/session/infrastructure/VirtualConversationDatabase";
|
||||
import { SessionController } from "./core/session/presentation/SessionController";
|
||||
import { SessionMetaService } from "./core/session/services/SessionMetaService";
|
||||
import { honoApp } from "./hono/app";
|
||||
import { InitializeService } from "./hono/initialize";
|
||||
import { routes } from "./hono/route";
|
||||
import { platformLayer } from "./lib/effect/layers";
|
||||
|
||||
// biome-ignore lint/style/noProcessEnv: allow only here
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
if (!isDevelopment) {
|
||||
const staticPath = resolve(import.meta.dirname, "static");
|
||||
console.log("Serving static files from ", staticPath);
|
||||
|
||||
honoApp.use(
|
||||
"/assets/*",
|
||||
serveStatic({
|
||||
root: staticPath,
|
||||
}),
|
||||
);
|
||||
|
||||
honoApp.use("*", async (c, next) => {
|
||||
if (c.req.path.startsWith("/api")) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const html = await readFile(resolve(staticPath, "index.html"), "utf-8");
|
||||
return c.html(html);
|
||||
});
|
||||
}
|
||||
|
||||
const program = routes(honoApp)
|
||||
// 依存の浅い順にコンテナに pipe する必要がある
|
||||
.pipe(
|
||||
/** Presentation */
|
||||
Effect.provide(ProjectController.Live),
|
||||
Effect.provide(SessionController.Live),
|
||||
Effect.provide(GitController.Live),
|
||||
Effect.provide(ClaudeCodeController.Live),
|
||||
Effect.provide(ClaudeCodeSessionProcessController.Live),
|
||||
Effect.provide(ClaudeCodePermissionController.Live),
|
||||
Effect.provide(FileSystemController.Live),
|
||||
Effect.provide(SSEController.Live),
|
||||
Effect.provide(SchedulerController.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Application */
|
||||
Effect.provide(InitializeService.Live),
|
||||
Effect.provide(FileWatcherService.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Domain */
|
||||
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
||||
Effect.provide(ClaudeCodePermissionService.Live),
|
||||
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
||||
Effect.provide(ClaudeCodeService.Live),
|
||||
Effect.provide(GitService.Live),
|
||||
Effect.provide(SchedulerService.Live),
|
||||
Effect.provide(SchedulerConfigBaseDir.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Infrastructure */
|
||||
Effect.provide(ProjectRepository.Live),
|
||||
Effect.provide(SessionRepository.Live),
|
||||
Effect.provide(ProjectMetaService.Live),
|
||||
Effect.provide(SessionMetaService.Live),
|
||||
Effect.provide(VirtualConversationDatabase.Live),
|
||||
)
|
||||
.pipe(
|
||||
/** Platform */
|
||||
Effect.provide(platformLayer),
|
||||
Effect.provide(NodeContext.layer),
|
||||
);
|
||||
|
||||
await Effect.runPromise(program);
|
||||
|
||||
const port = isDevelopment
|
||||
? // biome-ignore lint/style/noProcessEnv: allow only here
|
||||
(process.env.DEV_BE_PORT ?? "3401")
|
||||
: // biome-ignore lint/style/noProcessEnv: allow only here
|
||||
(process.env.PORT ?? "3000");
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: honoApp.fetch,
|
||||
port: parseInt(port, 10),
|
||||
},
|
||||
(info) => {
|
||||
console.log(`Server is running on http://localhost:${info.port}`);
|
||||
},
|
||||
);
|
||||
@@ -9,31 +9,17 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
// build optimization
|
||||
"incremental": true,
|
||||
// plugins
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
// paths alias
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
// typecheck
|
||||
"types": ["node", "vitest/globals"],
|
||||
"types": ["node", "vitest/globals", "vite/client"],
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
"./.next/types/**/*.ts"
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
35
vite.config.ts
Normal file
35
vite.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { lingui } from "@lingui/vite-plugin";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react-swc";
|
||||
import dotenv from "dotenv";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
dotenv.config({ path: "../../.env.local" });
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
viteReact(),
|
||||
lingui(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "dist/static",
|
||||
},
|
||||
server: {
|
||||
port: parseInt(process.env.DEV_FE_PORT ?? "3400", 10),
|
||||
proxy: {
|
||||
"/api": `http://localhost:${process.env.DEV_BE_PORT ?? "3401"}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user