const API_CONFIG = {
local: {
http: "http://localhost:8080",
ws: "ws://localhost:8080",
},
production: {
http: "https://api.makima.jp",
ws: "wss://api.makima.jp",
},
} as const;
type Environment = "local" | "production";
function detectEnvironment(): Environment {
// Check if explicitly set via env var
const envOverride = import.meta.env.VITE_API_ENV as Environment | undefined;
if (envOverride && (envOverride === "local" || envOverride === "production")) {
return envOverride;
}
// Auto-detect based on hostname
if (typeof window !== "undefined") {
const hostname = window.location.hostname;
if (hostname === "localhost" || hostname === "127.0.0.1") {
return "local";
}
}
return "production";
}
const env = detectEnvironment();
export const API_BASE = API_CONFIG[env].http;
export const WS_BASE = API_CONFIG[env].ws;
export const LISTEN_ENDPOINT = `${WS_BASE}/api/v1/listen`;
export const FILE_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/files/subscribe`;
export function getEnvironment(): Environment {
return env;
}
// File API types
export interface TranscriptEntry {
id: string;
speaker: string;
start: number;
end: number;
text: string;
isFinal: boolean;
}
// Chart types for visualization
export type ChartType = "line" | "bar" | "pie" | "area";
// Body element types for structured content
export type BodyElement =
| { type: "heading"; level: number; text: string }
| { type: "paragraph"; text: string }
| {
type: "chart";
chartType: ChartType;
title?: string;
data: Record<string, unknown>[];
config?: Record<string, unknown>;
}
| { type: "image"; src: string; alt?: string; caption?: string };
export interface FileSummary {
id: string;
name: string;
description: string | null;
transcriptCount: number;
duration: number | null;
version: number;
createdAt: string;
updatedAt: string;
}
export interface FileDetail {
id: string;
ownerId: string;
name: string;
description: string | null;
transcript: TranscriptEntry[];
location: string | null;
summary: string | null;
body: BodyElement[];
version: number;
createdAt: string;
updatedAt: string;
}
export interface FileListResponse {
files: FileSummary[];
total: number;
}
export interface CreateFileRequest {
name?: string;
description?: string;
transcript: TranscriptEntry[];
location?: string;
}
export interface UpdateFileRequest {
name?: string;
description?: string;
transcript?: TranscriptEntry[];
summary?: string;
body?: BodyElement[];
version?: number;
}
// Conflict error types
export interface ConflictErrorResponse {
code: "VERSION_CONFLICT";
message: string;
expectedVersion: number;
actualVersion: number;
}
export class VersionConflictError extends Error {
expectedVersion: number;
actualVersion: number;
constructor(conflict: ConflictErrorResponse) {
super(conflict.message);
this.name = "VersionConflictError";
this.expectedVersion = conflict.expectedVersion;
this.actualVersion = conflict.actualVersion;
}
}
// Available LLM models
export type LlmModel = "claude-sonnet" | "claude-opus" | "groq";
// Chat API types
export interface ChatRequest {
message: string;
model?: LlmModel;
}
export interface ToolCallInfo {
name: string;
result: {
success: boolean;
message: string;
};
}
export interface ChatResponse {
response: string;
toolCalls: ToolCallInfo[];
updatedBody: BodyElement[];
updatedSummary: string | null;
}
// File API functions
export async function listFiles(): Promise<FileListResponse> {
const res = await fetch(`${API_BASE}/api/v1/files`);
if (!res.ok) {
throw new Error(`Failed to list files: ${res.statusText}`);
}
return res.json();
}
export async function getFile(id: string): Promise<FileDetail> {
const res = await fetch(`${API_BASE}/api/v1/files/${id}`);
if (!res.ok) {
throw new Error(`Failed to get file: ${res.statusText}`);
}
return res.json();
}
export async function createFile(data: CreateFileRequest): Promise<FileDetail> {
const res = await fetch(`${API_BASE}/api/v1/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Failed to create file: ${res.statusText}`);
}
return res.json();
}
export async function updateFile(
id: string,
data: UpdateFileRequest
): Promise<FileDetail> {
const res = await fetch(`${API_BASE}/api/v1/files/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.status === 409) {
const conflict = (await res.json()) as ConflictErrorResponse;
throw new VersionConflictError(conflict);
}
if (!res.ok) {
throw new Error(`Failed to update file: ${res.statusText}`);
}
return res.json();
}
export async function deleteFile(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/v1/files/${id}`, {
method: "DELETE",
});
if (!res.ok) {
throw new Error(`Failed to delete file: ${res.statusText}`);
}
}
// Chat API function
export async function chatWithFile(
id: string,
message: string,
model?: LlmModel
): Promise<ChatResponse> {
const body: ChatRequest = { message };
if (model) {
body.model = model;
}
const res = await fetch(`${API_BASE}/api/v1/files/${id}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Chat failed: ${errorText || res.statusText}`);
}
return res.json();
}