Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion packages/server/increa_reader/config_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
Configuration API routes
"""

import os
import platform
import sys
from pathlib import Path

from fastapi import FastAPI
from pydantic import BaseModel

from .models import RepoItem, WorkspaceConfig
from .workspace import load_api_settings, save_api_settings, save_workspace_config
from .workspace import (
is_onboarding_complete,
load_api_settings,
mark_onboarding_complete,
save_api_settings,
save_workspace_config,
)


class RepoEntry(BaseModel):
Expand All @@ -25,6 +34,30 @@ class ApiSettingsRequest(BaseModel):
default_model: str | None = None


class CompleteOnboardingRequest(BaseModel):
repo_path: str
api_key: str | None = None
base_url: str | None = None
auth_token: str | None = None
default_model: str | None = None


def _has_real_value(value: str | None) -> bool:
if not value:
return False
normalized = value.strip().strip('"').strip("'")
return bool(normalized and normalized not in {"your-api-key", "your-proxy-token"})


def _check_pdf_support() -> dict:
try:
import fitz # type: ignore

return {"ok": True, "detail": f"PyMuPDF {fitz.version[0]}"}
except Exception as exc:
return {"ok": False, "detail": str(exc)}


def _mask_api_key(key: str | None) -> str | None:
"""Mask API key for display: 'sk-ant-api03-xxxxx' → 'sk-ant-a...yyyy'"""
if not key or len(key) < 12:
Expand Down Expand Up @@ -112,3 +145,57 @@ async def update_api_settings(request: ApiSettingsRequest):
"api_key": _mask_api_key(updated.get("api_key")),
"default_model": updated["default_model"],
}

@app.get("/api/config/onboarding")
async def get_onboarding_state():
"""Return first-run setup state and environment diagnostics"""
settings = load_api_settings()
has_auth = any(
[
_has_real_value(settings.get("api_key")),
_has_real_value(os.getenv("ANTHROPIC_API_KEY")),
_has_real_value(os.getenv("ANTHROPIC_AUTH_TOKEN")),
]
)
valid_repos = [repo for repo in workspace_config.repos if Path(repo.root).exists()]
return {
"completed": is_onboarding_complete(),
"needs_onboarding": not is_onboarding_complete() or not valid_repos,
"has_repos": bool(valid_repos),
"has_api_credentials": has_auth,
"diagnostics": {
"backend": {"ok": True, "detail": "FastAPI server is reachable"},
"python": {
"ok": sys.version_info >= (3, 10),
"detail": f"Python {platform.python_version()}",
},
"pdf": _check_pdf_support(),
},
}

@app.post("/api/config/onboarding/complete")
async def complete_onboarding(request: CompleteOnboardingRequest):
"""Save first-run setup settings and mark onboarding complete"""
path_obj = Path(request.repo_path).expanduser().resolve()
repo = RepoItem(name=path_obj.name, root=str(path_obj))
save_workspace_config([repo])
workspace_config.repos.clear()
workspace_config.repos.append(repo)

api_key = request.api_key.strip() if request.api_key else None
auth_token = request.auth_token.strip() if request.auth_token else None
settings = {
"base_url": request.base_url.strip() if request.base_url else None,
"api_key": api_key or auth_token,
"default_model": request.default_model.strip() if request.default_model else None,
}
save_api_settings(settings)
mark_onboarding_complete()
return {
"repo": {"name": repo.name, "root": repo.root, "exists": path_obj.exists()},
"api_settings": {
"base_url": settings["base_url"],
"api_key": _mask_api_key(settings.get("api_key")),
"default_model": settings["default_model"],
},
}
12 changes: 12 additions & 0 deletions packages/server/increa_reader/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ def save_api_settings(settings: dict) -> None:
save_raw_config(data)


def mark_onboarding_complete() -> None:
"""Persist that the first-run setup has been completed"""
data = load_raw_config()
data["onboarding_completed"] = True
save_raw_config(data)


def is_onboarding_complete() -> bool:
"""Return whether first-run setup has been completed"""
return bool(load_raw_config().get("onboarding_completed"))


def _load_repos_from_config() -> List[RepoItem] | None:
"""Try loading repos from config.json, return None if not found"""
data = load_raw_config()
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/app/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,26 @@ export type ApiSettings = {
default_model: string | null
}

export type OnboardingState = {
completed: boolean
needs_onboarding: boolean
has_repos: boolean
has_api_credentials: boolean
diagnostics: {
backend: { ok: boolean; detail: string }
python: { ok: boolean; detail: string }
pdf: { ok: boolean; detail: string }
}
}

export type CompleteOnboardingRequest = {
repo_path: string
api_key?: string | null
base_url?: string | null
auth_token?: string | null
default_model?: string | null
}

export async function fetchApiSettings(): Promise<ApiSettings> {
const response = await fetch('/api/config/api-settings')
return response.json()
Expand All @@ -157,4 +177,21 @@ export async function updateApiSettings(settings: Partial<ApiSettings>): Promise
return response.json()
}

export async function fetchOnboardingState(): Promise<OnboardingState> {
const response = await fetch('/api/config/onboarding')
return response.json()
}

export async function completeOnboarding(settings: CompleteOnboardingRequest): Promise<void> {
const response = await fetch('/api/config/onboarding/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.detail || 'Failed to complete onboarding')
}
}

export type { RepoConfigInfo, RepoInfo, RepoTreeData, TreeNode }
13 changes: 4 additions & 9 deletions packages/ui/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { Route, Routes } from 'react-router-dom'
import { VisibleContentProvider } from '../contexts/visible-content-context'
import { BoardViewer } from './board-viewer'
import { KnowledgeMap } from './knowledge-map'
import { Layout } from './layout'
import { OnboardingPage } from './onboarding'
import { TabbedViewer } from './tabs/tabbed-viewer'

function HomePage() {
return (
<div className="p-8">
<h1 className="text-2xl font-bold">AI Chat</h1>
</div>
)
}

function App() {
return (
<VisibleContentProvider>
<Routes>
<Route path="/onboarding" element={<OnboardingPage />} />
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/" element={<KnowledgeMap />} />
<Route path="/board" element={<BoardViewer />} />
<Route path="/views/:repoName/*" element={<TabbedViewer />} />
</Route>
Expand Down
Loading