test_workflow
Some checks failed
Backend ECR Deployment / build-and-push (push) Has been cancelled
Backend ECR Deployment / notify-deploy (push) Has been cancelled
Backend Modal Build / notify-deploy (push) Has been cancelled
Backend Modal Build / Build & Push via Modal (push) Has been cancelled

This commit is contained in:
DatTT127
2026-07-18 17:48:19 +07:00
parent 7d5c583475
commit 57a8bac1be
87 changed files with 36291 additions and 155 deletions

View File

@@ -0,0 +1,30 @@
# Stage 1: Build
FROM node:26-bookworm-slim AS builder
WORKDIR /app
# Copy package files and install scripts first (better cache)
COPY package.json package-lock.json* ./
COPY scripts/ ./scripts/
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Stage 2: Runtime (nginx)
FROM nginx:alpine
# Remove default nginx static files
RUN rm -rf /usr/share/nginx/html/*
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Custom nginx config for SPA routing + caching
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,13 +1,25 @@
# Frontend application configuration
# This is the single source of truth for frontend feature flags and URLs.
# Edit this file directly instead of using .env / .env.development.
#
# Deployment note: the backend servers (main.py, cv_inference_server.py) enforce
# CORS via the CORS_ORIGINS environment variable. Make sure the origins listed here
# (especially the production frontend origin) are included in that env var.
VITE_USE_BACKEND_SEGMENTATION: "true"
VITE_SEGMENT_API_BASE: ""
# Leave empty in dev — Vite proxy handles /api/* → backend.
# Set to your production API origin (e.g. "https://api.your-domain.com") for builds.
VITE_API_BASE_URL: "https://api.lumina-msk.io.vn"
# Leave empty in dev — Vite proxy handles /api/test/* → CV inference server.
# Set to your production CV inference origin (e.g. "https://cv.your-domain.com") for builds.
VITE_SEGMENT_API_BASE: "https://cv.lumina-msk.io.vn"
# Comma-separated list of origins the frontend may be served from.
# Used by deployment scripts to populate backend CORS_ORIGINS env var.
VITE_ALLOWED_ORIGINS: "https://lumina-msk.io.vn,https://www.lumina-msk.io.vn"
VITE_USE_CV_CELERY: "false"
VITE_API_BASE_URL: ""
VITE_CLINICAL_CHAT_USE_LLM: "true"
VITE_CLINICAL_CHAT_MOCK_TOOLS: "true"
VITE_OLLAMA_CHAT_URL: "/api/ollama-chat/api/chat"
VITE_OLLAMA_MODEL: "gemma4:e4b"
VITE_MODAL_OLLAMA_TARGET: "https://dtj-tran--ollama-gemma4-e4b-ollamaserver-web.modal.run"
VITE_DISTRIBUTE_DIRRECTORY: "workspace/sprint_1_2/CODEBASE/frontend/distribute_asset"

View File

@@ -1,16 +1,24 @@
import React, { Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { PatientStoreProvider } from './lib/patientStore';
import PatientWorklistPage from './pages/PatientWorklistPage';
import ClinicalWorkspacePage from './pages/ClinicalWorkspacePage';
const PatientWorklistPage = React.lazy(() => import('./pages/PatientWorklistPage'));
const ClinicalWorkspacePage = React.lazy(() => import('./pages/ClinicalWorkspacePage'));
function LoadingFallback() {
return <div style={{ padding: '2rem', textAlign: 'center' }}>Loading</div>;
}
export default function App() {
return (
<PatientStoreProvider>
<Routes>
<Route path="/" element={<PatientWorklistPage />} />
<Route path="/workspace/:patientId" element={<ClinicalWorkspacePage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<PatientWorklistPage />} />
<Route path="/workspace/:patientId" element={<ClinicalWorkspacePage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</PatientStoreProvider>
);
}

View File

@@ -19,8 +19,12 @@ export async function probeCapabilities(): Promise<CapabilityReport> {
if (!secureContext) {
notes.push('Not a secure context. Use https:// or http://localhost.');
}
if (location.hostname === '127.0.0.1') {
notes.push('OPFS is origin-scoped: 127.0.0.1 and localhost use separate storage.');
if (opfs && location.protocol !== 'https:' && location.hostname !== 'localhost') {
notes.push(
'OPFS is origin-scoped: caches are isolated per scheme+host+port. ' +
'Models downloaded on one origin (e.g. localhost, 127.0.0.1, preview domains) ' +
'are not available on another. Expect a full re-download when the origin changes.',
);
}
const ready = webgpu && opfs && worker && secureContext;

View File

@@ -14,13 +14,13 @@ export const BOOTSTRAP_DECODE: DecodeParams = {
maxTokens: MIN_COT_MAX_TOKENS,
};
function envFlag(name: string, defaultValue: boolean): boolean {
const raw = import.meta.env[name];
if (raw === undefined || raw === '') {
return defaultValue;
}
return raw === '1' || raw.toLowerCase() === 'true';
}
// function envFlag(name: string, defaultValue: boolean): boolean {
// const raw = import.meta.env[name];
// if (raw === undefined || raw === '') {
// return defaultValue;
// }
// return raw === '1' || raw.toLowerCase() === 'true';
// }
/** Try local Gemma worker when OPFS model is available. Falls back to mock replies otherwise. */
export function useLocalLlmWhenAvailable(): boolean {
@@ -33,7 +33,19 @@ export function useMockAgentTools(): boolean {
}
export function clinicalChatBffBaseUrl(): string {
return import.meta.env.VITE_API_BASE_URL ?? '';
const raw = import.meta.env.VITE_API_BASE_URL;
const trimmed = raw != null ? String(raw).trim() : '';
if (trimmed) {
return trimmed;
}
if (import.meta.env.DEV) {
return '';
}
console.warn(
'[clinicalChatConfig] VITE_API_BASE_URL is not set. ' +
'Clinical chat BFF calls will fail. Set it in config/frontend.config.yaml for production builds.',
);
return '';
}
const DEFAULT_OLLAMA_CHAT_URL = '/api/ollama-chat/api/chat';

View File

@@ -216,7 +216,7 @@ export async function runDirectChatTurn(
promptOptions,
decode,
input.onToken,
input.onSegmentStart,
// input.onSegmentStart,
);
if (signal?.aborted) {

View File

@@ -71,7 +71,7 @@ export interface SegmentationApiResult {
}
/** Resolve API base: dev uses Vite proxy (empty); preview/build targets local test server. */
/** Resolve API base: dev uses Vite proxy (empty); production uses configured URL. */
export function getSegmentApiBase(): string {
const configured = import.meta.env.VITE_SEGMENT_API_BASE;
if (configured != null && String(configured).trim() !== '') {
@@ -80,7 +80,11 @@ export function getSegmentApiBase(): string {
if (import.meta.env.DEV) {
return '';
}
return 'http://127.0.0.1:8001';
console.warn(
'[segmentationApi] VITE_SEGMENT_API_BASE is not set. ' +
'Segmentation API calls will fail. Set it in config/frontend.config.yaml for production builds.',
);
return '';
}
/** ML inference always uses the test proxy / production API — mock path detached. */

File diff suppressed because one or more lines are too long

View File

@@ -24,6 +24,30 @@ const MODAL_OLLAMA_TARGET =
frontendConfig.VITE_MODAL_OLLAMA_TARGET ??
'https://dtj-tran--ollama-gemma4-e4b-ollamaserver-web.modal.run';
// API base URLs from YAML — empty in dev so Vite proxy handles them.
const apiBaseUrl = (frontendConfig.VITE_API_BASE_URL ?? '').trim();
const segmentApiBase = (frontendConfig.VITE_SEGMENT_API_BASE ?? '').trim();
// Resolve build output directory from YAML config, falling back to 'dist'
function findProjectRoot(startDir: string): string {
let current = startDir;
while (current !== path.parse(current).root) {
if (fs.existsSync(path.join(current, '.git'))) {
return current;
}
current = path.dirname(current);
}
return startDir;
}
const projectRoot = findProjectRoot(__dirname);
const configuredOutDir = frontendConfig.VITE_DISTRIBUTE_DIRRECTORY;
const buildOutDir = configuredOutDir
? path.isAbsolute(configuredOutDir)
? configuredOutDir
: path.resolve(projectRoot, configuredOutDir)
: path.resolve(__dirname, 'dist');
export default defineConfig({
plugins: [react()],
worker: {
@@ -33,7 +57,7 @@ export default defineConfig({
alias: {
'@vkist/agent-runtime': path.resolve(
__dirname,
'../../ml/implementation/nlp/agent_runtime/src/index.ts',
'../../ml/implementation/nlp/agent_runtime/src'
),
},
},
@@ -41,7 +65,7 @@ export default defineConfig({
host: '127.0.0.1',
port: 5173,
strictPort: true,
// Keep HMR WebSocket on the same host/port as the page (avoids localhost vs 127.0.0.1 mismatch).
// Keep HMR WebSocket consistent (avoid localhost vs 127.0.0.1 mismatch).
hmr: {
host: '127.0.0.1',
port: 5173,
@@ -58,18 +82,31 @@ export default defineConfig({
proxyTimeout: 300_000,
},
'/api/test': {
target: 'http://127.0.0.1:8001',
target: segmentApiBase || 'http://127.0.0.1:8001',
changeOrigin: true,
timeout: 300_000,
proxyTimeout: 300_000,
},
'/api': {
target: 'http://127.0.0.1:8001',
target: apiBaseUrl || 'http://127.0.0.1:8001',
changeOrigin: true,
timeout: 300_000,
proxyTimeout: 300_000,
},
},
},
build: {
outDir: buildOutDir,
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ml': ['@litert-lm/core', '@mediapipe/tasks-genai'],
'agent-runtime': ['@vkist/agent-runtime'],
},
},
},
chunkSizeWarningLimit: 250,
},
define: defineVars,
});