Feat: CRM v2, Richer synthetic data, Canvas JSON Components

This commit is contained in:
Sagnik
2026-04-23 22:00:44 +05:30
parent 6cdc366718
commit f04571bd7b
54 changed files with 89916 additions and 578 deletions

View File

@@ -15,7 +15,7 @@
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Send, Mic, Kanban, Users, Phone, MapPinned, CalendarClock, ChevronDown, History, BarChart2 } from 'lucide-react';
import { Send, Mic, Kanban, Users, Phone, MapPinned, CalendarClock, ChevronDown, History, BarChart2, Database } from 'lucide-react';
import { Input } from '@/components/ui/input';
import type { CanvasPageRevision, MergeRequest, UserProfile } from '@/oracle/types/canvas';
import type { ComponentRenderContext } from '@/oracle/components/ComponentRegistry';
@@ -23,6 +23,7 @@ import { useOraclePage } from '@/oracle/hooks/useOraclePage';
import { useOracleExecution } from '@/oracle/hooks/useOracleExecution';
import { BranchBar } from '@/oracle/components/BranchBar';
import { CanvasViewport } from '@/oracle/components/CanvasViewport';
import { ClientDataLens } from '@/oracle/components/ClientDataLens';
import { PromptRail } from '@/oracle/components/PromptRail';
import { ShareModal } from '@/oracle/components/ShareModal';
import { RollbackConfirmModal } from '@/oracle/components/RollbackConfirmModal';
@@ -68,6 +69,7 @@ export default function OraclePage() {
const [listening, setListening] = useState(false);
const [railOpen, setRailOpen] = useState(false);
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
const [activeSubtab, setActiveSubtab] = useState<'canvas' | 'client-data'>('canvas');
// Overlay state
const [shareOpen, setShareOpen] = useState(false);
@@ -229,8 +231,31 @@ export default function OraclePage() {
onOpenMergeReview={() => void handleOpenMergeReview()}
/>
<div className="relative z-20 flex flex-shrink-0 gap-2 px-5 pt-3">
<button
type="button"
onClick={() => setActiveSubtab('canvas')}
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs font-semibold transition ${
activeSubtab === 'canvas' ? 'border-blue-400/35 bg-blue-500/15 text-blue-100' : 'border-white/10 bg-white/[0.03] text-zinc-500 hover:text-zinc-200'
}`}
>
<BarChart2 className="h-3.5 w-3.5" />
Oracle Canvas
</button>
<button
type="button"
onClick={() => setActiveSubtab('client-data')}
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs font-semibold transition ${
activeSubtab === 'client-data' ? 'border-cyan-400/35 bg-cyan-500/15 text-cyan-100' : 'border-white/10 bg-white/[0.03] text-zinc-500 hover:text-zinc-200'
}`}
>
<Database className="h-3.5 w-3.5" />
Client Data
</button>
</div>
{/* ── AI Insight strip ───────────────────────────────────────────────── */}
{page && history.length > 0 && history[history.length - 1].execution.summary && (
{activeSubtab === 'canvas' && page && history.length > 0 && history[history.length - 1].execution.summary && (
<div className="relative z-10 px-4 pt-2 pb-1.5 flex-shrink-0">
<div
className="flex items-stretch rounded-xl overflow-hidden"
@@ -253,6 +278,10 @@ export default function OraclePage() {
{/* ── Main content area: canvas + rail ───────────────────────────────── */}
<div className="relative z-10 flex-1 flex overflow-hidden min-h-0">
{activeSubtab === 'client-data' ? (
<ClientDataLens />
) : (
<>
{/* Canvas viewport */}
<CanvasViewport
components={components}
@@ -269,10 +298,12 @@ export default function OraclePage() {
isOpen={railOpen}
onToggle={() => setRailOpen((p) => !p)}
/>
</>
)}
</div>
{/* ── Floating Prompt Bar ─────────────────────────────────────────────── */}
<div className="absolute inset-x-0 bottom-8 z-30 flex justify-center px-4">
{activeSubtab === 'canvas' && <div className="absolute inset-x-0 bottom-8 z-30 flex justify-center px-4">
<div className="relative w-full max-w-3xl">
{/* Blue glow */}
<div
@@ -447,7 +478,7 @@ export default function OraclePage() {
</div>
</div>
</div>
</div>
</div>}
{/* ── Overlays ────────────────────────────────────────────────────────── */}
<ShareModal

View File

@@ -13,6 +13,9 @@ import type {
ImportProposal,
ImportReviewDecision,
QdScoreEntry,
OracleClientDataListItem,
OracleClientDataDetail,
OracleClientTimelineItem,
} from '@/types/crmTypes';
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
@@ -218,3 +221,41 @@ export async function commitImportBatch(batchId: string): Promise<{
}>(`/api/crm/imports/${batchId}/commit`, { method: 'POST' });
return res.data;
}
export async function fetchOracleClientData(params?: {
search?: string;
limit?: number;
offset?: number;
}): Promise<{ items: OracleClientDataListItem[]; count: number }> {
const qs = new URLSearchParams();
if (params?.search) qs.set('search', params.search);
if (params?.limit != null) qs.set('limit', String(params.limit));
if (params?.offset != null) qs.set('offset', String(params.offset));
const res = await apiFetch<{ status: string; data: OracleClientDataListItem[]; meta?: { count?: number } }>(
`/api/crm/client-data?${qs}`,
);
return { items: res.data, count: res.meta?.count ?? res.data.length };
}
export async function fetchOracleClientDataDetail(personId: string): Promise<OracleClientDataDetail> {
const res = await apiFetch<{ status: string; data: OracleClientDataDetail }>(`/api/crm/client-data/${personId}`);
return res.data;
}
export async function patchOracleClientData(
personId: string,
patch: Record<string, string | null>,
): Promise<{ person_id: string; updated: string[] }> {
const res = await apiFetch<{ status: string; data: { person_id: string; updated: string[] } }>(
`/api/crm/client-data/${personId}`,
{ method: 'PATCH', body: JSON.stringify(patch) },
);
return res.data;
}
export async function fetchOracleClientTimeline(personId: string): Promise<OracleClientTimelineItem[]> {
const res = await apiFetch<{ status: string; data: OracleClientTimelineItem[] }>(
`/api/crm/client-data/${personId}/timeline`,
);
return res.data;
}

View File

@@ -0,0 +1,392 @@
import { useEffect, useMemo, useState } from 'react';
import {
Activity,
AlertTriangle,
ArrowRight,
Check,
Clock,
Database,
Mail,
Phone,
Search,
Sparkles,
UserRound,
Zap,
} from 'lucide-react';
import { Input } from '@/components/ui/input';
import {
fetchOracleClientData,
fetchOracleClientDataDetail,
patchOracleClientData,
} from '@/lib/crmApi';
import type { OracleClientDataDetail, OracleClientDataListItem } from '@/types/crmTypes';
const STAGES = [
'new',
'contacted',
'qualified',
'site_visit_scheduled',
'site_visited',
'negotiation',
'booking_initiated',
'booked',
];
function fmt(value: unknown): string {
if (value == null || value === '') return '-';
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
return String(value);
}
function shortDate(value: unknown): string {
if (!value) return '-';
const date = new Date(String(value));
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString();
}
function FieldRow({ label, value }: { label: string; value: unknown }) {
return (
<div className="grid grid-cols-[150px_1fr] gap-3 border-b border-white/[0.06] py-2.5 text-sm">
<span className="text-zinc-500">{label}</span>
<span className="min-w-0 break-words text-zinc-200">{fmt(value)}</span>
</div>
);
}
function EmptyDiagnostic({ error, loading }: { error: string | null; loading: boolean }) {
return (
<div className="flex h-full min-h-[420px] items-center justify-center p-8">
<div className="max-w-xl rounded-3xl border border-amber-400/20 bg-amber-500/[0.08] p-6">
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-2xl border border-amber-300/25 bg-amber-400/10">
{loading ? <Database className="h-5 w-5 animate-pulse text-amber-200" /> : <AlertTriangle className="h-5 w-5 text-amber-200" />}
</div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-amber-200">Client data unavailable</p>
<h3 className="mt-2 text-xl font-semibold text-zinc-50">
{loading ? 'Loading Velocity CRM data...' : 'The CRM lens has no rows to show.'}
</h3>
<p className="mt-3 text-sm leading-6 text-zinc-400">
{error
? error
: 'The local frontend is alive, but the CRM API returned no clients. Start the local backend, start Docker/Postgres, apply the canonical schema, and seed synthetic_crm_v2 before verifying this tab.'}
</p>
<div className="mt-5 rounded-2xl border border-white/10 bg-black/25 p-4 text-xs leading-6 text-zinc-400">
<p className="font-semibold text-zinc-200">Expected local stack</p>
<p>Backend: http://127.0.0.1:8001</p>
<p>DB: 127.0.0.1:54329 / velocity_local</p>
<p>Dataset: db assets/synthetic_crm_v2/csv</p>
</div>
</div>
</div>
);
}
export function ClientDataLens() {
const [query, setQuery] = useState('');
const [items, setItems] = useState<OracleClientDataListItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [detail, setDetail] = useState<OracleClientDataDetail | null>(null);
const [loadingList, setLoadingList] = useState(false);
const [loadingDetail, setLoadingDetail] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [draft, setDraft] = useState({
full_name: '',
primary_email: '',
primary_phone: '',
buyer_type: '',
communication_preference: '',
best_contact_time: '',
budget_band: '',
urgency: '',
});
useEffect(() => {
const handle = window.setTimeout(() => {
setLoadingList(true);
setError(null);
void fetchOracleClientData({ search: query, limit: 80 })
.then(({ items: rows }) => {
setItems(rows);
setSelectedId((current) => current ?? rows[0]?.person_id ?? null);
if (!rows.length) {
setDetail(null);
}
})
.catch((err) => {
setItems([]);
setSelectedId(null);
setDetail(null);
setError(err instanceof Error ? err.message : 'Failed to reach the CRM client data API.');
})
.finally(() => setLoadingList(false));
}, 180);
return () => window.clearTimeout(handle);
}, [query]);
useEffect(() => {
if (!selectedId) return;
setLoadingDetail(true);
setError(null);
void fetchOracleClientDataDetail(selectedId)
.then((data) => {
const profile = data.profile ?? {};
setDetail(data);
setDraft({
full_name: String(profile.full_name ?? ''),
primary_email: String(profile.primary_email ?? ''),
primary_phone: String(profile.primary_phone ?? ''),
buyer_type: String(profile.buyer_type ?? ''),
communication_preference: String(profile.communication_preference ?? ''),
best_contact_time: String(profile.best_contact_time ?? ''),
budget_band: String(profile.budget_band ?? ''),
urgency: String(profile.urgency ?? ''),
});
})
.catch((err) => {
setDetail(null);
setError(err instanceof Error ? err.message : 'Failed to load the selected client record.');
})
.finally(() => setLoadingDetail(false));
}, [selectedId]);
const selected = useMemo(
() => items.find((item) => item.person_id === selectedId) ?? items[0] ?? null,
[items, selectedId],
);
const profile = detail?.profile ?? {};
const currentStage = String(profile.lead_status ?? selected?.lead_status ?? 'new');
const qdScore = Number(selected?.qd_score ?? 0);
const activeStageIndex = Math.max(0, STAGES.indexOf(currentStage));
async function saveDraft() {
if (!selectedId) return;
setSaving(true);
setError(null);
try {
await patchOracleClientData(selectedId, draft);
const fresh = await fetchOracleClientDataDetail(selectedId);
setDetail(fresh);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save client data.');
} finally {
setSaving(false);
}
}
return (
<div className="relative z-10 grid h-full min-h-0 grid-cols-[360px_minmax(0,1fr)] gap-4 overflow-hidden px-5 pb-5 pt-3">
<aside className="flex min-h-0 flex-col overflow-hidden rounded-3xl border border-cyan-400/15 bg-[linear-gradient(180deg,rgba(8,47,73,0.16),rgba(0,0,0,0.22))]">
<div className="border-b border-white/10 p-5">
<div className="mb-4 flex items-start justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-cyan-300">Client Data</p>
<h2 className="mt-1 text-xl font-semibold text-zinc-50">Velocity CRM Lens</h2>
<p className="mt-1 text-xs text-zinc-500">Search, inspect, edit, and follow the client signal trail.</p>
</div>
<Sparkles className="h-5 w-5 text-cyan-200" />
</div>
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-zinc-500" />
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search name, phone, email, project..."
className="h-10 border-white/10 bg-black/25 pl-9 text-sm text-zinc-100"
/>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-2">
{items.map((item) => (
<button
key={item.person_id}
type="button"
onClick={() => setSelectedId(item.person_id)}
className={`mb-2 w-full rounded-2xl border p-3 text-left transition ${
item.person_id === selectedId
? 'border-cyan-300/40 bg-cyan-500/12 shadow-[0_0_24px_rgba(34,211,238,0.08)]'
: 'border-white/8 bg-white/[0.03] hover:bg-white/[0.06]'
}`}
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-2xl border border-blue-400/20 bg-blue-500/15 text-sm font-semibold text-blue-100">
{item.full_name.slice(0, 1)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-zinc-100">{item.full_name}</p>
<p className="truncate text-xs text-zinc-500">{item.projects || item.buyer_type || 'No project interest'}</p>
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-zinc-400">
<span className="rounded-full bg-cyan-500/10 px-2 py-0.5 text-cyan-300">QD {Math.round((item.qd_score || 0) * 100)}</span>
<span>{item.days_since_contact == null ? 'No contact clock' : `${item.days_since_contact}d since contact`}</span>
</div>
</div>
</div>
</button>
))}
{!items.length && (
<div className="px-4 py-7 text-sm text-zinc-500">
{loadingList ? 'Loading clients...' : 'No clients found.'}
</div>
)}
</div>
</aside>
<main className="min-w-0 overflow-hidden rounded-3xl border border-white/10 bg-black/20">
{!selected ? (
<EmptyDiagnostic error={error} loading={loadingList} />
) : (
<div className="flex h-full min-h-0 flex-col">
<section className="border-b border-white/10 bg-[linear-gradient(135deg,rgba(14,116,144,0.14),rgba(15,23,42,0.35))] p-5">
{error && (
<div className="mb-4 rounded-2xl border border-amber-400/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-100">
{error}
</div>
)}
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-[0.24em] text-zinc-500">Contact Record</p>
<h1 className="mt-1 text-3xl font-semibold text-zinc-50">{selected.full_name}</h1>
<div className="mt-3 flex flex-wrap gap-4 text-sm text-zinc-400">
<span className="flex items-center gap-1.5"><Phone className="h-4 w-4" />{fmt(selected.primary_phone)}</span>
<span className="flex items-center gap-1.5"><Mail className="h-4 w-4" />{fmt(selected.primary_email)}</span>
<span className="flex items-center gap-1.5"><UserRound className="h-4 w-4" />{fmt(selected.broker_name)}</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="rounded-2xl border border-cyan-400/20 bg-cyan-500/10 px-4 py-3 text-right">
<p className="text-xs text-cyan-300">QD Signal</p>
<p className="text-3xl font-semibold text-cyan-100">{Math.round(qdScore * 100)}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-black/25 px-4 py-3">
<p className="text-xs text-zinc-500">Last Contact</p>
<p className="mt-1 max-w-[160px] truncate text-sm font-semibold text-zinc-100">{shortDate(selected.last_contact_at)}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-black/25 px-4 py-3">
<p className="text-xs text-zinc-500">Next Action</p>
<p className="mt-1 max-w-[180px] truncate text-sm font-semibold text-zinc-100">{fmt(selected.next_best_action)}</p>
</div>
</div>
</div>
<div className="mt-5 grid grid-cols-8 overflow-hidden rounded-full border border-white/10 bg-white/[0.04]">
{STAGES.map((stage, index) => {
const active = activeStageIndex >= index;
return (
<div key={stage} className={`py-2 text-center text-[11px] font-semibold ${active ? 'bg-blue-500/70 text-white' : 'text-zinc-500'}`}>
{stage.replace(/_/g, ' ')}
</div>
);
})}
</div>
</section>
<section className="min-h-0 flex-1 overflow-y-auto p-5">
{loadingDetail && <p className="mb-4 text-sm text-zinc-500">Loading record...</p>}
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_410px]">
<div className="space-y-5">
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-100">Editable Details</h3>
<p className="text-xs text-zinc-500">Typed API writes only. Oracle SQL remains read-only.</p>
</div>
<button
type="button"
onClick={() => void saveDraft()}
disabled={saving}
className="rounded-xl bg-blue-500 px-4 py-2 text-xs font-semibold text-white disabled:opacity-50"
>
<Check className="mr-1 inline h-3.5 w-3.5" />
{saving ? 'Saving...' : 'Save changes'}
</button>
</div>
<div className="grid gap-3 md:grid-cols-2">
{Object.entries(draft).map(([key, value]) => (
<label key={key} className="text-xs text-zinc-500">
{key.replace(/_/g, ' ')}
<Input
value={value}
onChange={(event) => setDraft((current) => ({ ...current, [key]: event.target.value }))}
className="mt-1 h-9 border-white/10 bg-black/25 text-sm text-zinc-100"
/>
</label>
))}
</div>
</div>
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<h3 className="mb-4 text-base font-semibold text-zinc-100">Property Interests</h3>
<div className="grid gap-3 md:grid-cols-2">
{(detail?.property_interests ?? []).slice(0, 8).map((interest, index) => (
<div key={String(interest.interest_id ?? index)} className="rounded-2xl border border-white/10 bg-black/25 p-4">
<p className="font-medium text-zinc-100">{fmt(interest.project_name)}</p>
<p className="mt-1 text-xs text-zinc-500">{fmt(interest.configuration)} | {fmt(interest.budget_min)}-{fmt(interest.budget_max)}</p>
<p className="mt-2 text-xs text-cyan-300">Priority {fmt(interest.priority)}</p>
</div>
))}
{!(detail?.property_interests ?? []).length && <p className="text-sm text-zinc-500">No property interests loaded for this client.</p>}
</div>
</div>
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<h3 className="mb-4 text-base font-semibold text-zinc-100">Unified Engagement Timeline</h3>
<div className="space-y-3">
{(detail?.timeline ?? []).slice(0, 24).map((event) => (
<div key={event.id} className="flex gap-3 rounded-2xl border border-white/8 bg-black/25 p-3">
<Activity className="mt-1 h-4 w-4 text-blue-300" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-zinc-100">{fmt(event.title || event.type)}</p>
<p className="mt-1 text-xs text-zinc-500">{fmt(event.summary)}</p>
</div>
<span className="whitespace-nowrap text-[11px] text-zinc-600">{shortDate(event.date)}</span>
</div>
))}
{!(detail?.timeline ?? []).length && <p className="text-sm text-zinc-500">No timeline events loaded. Seed v2 interactions/messages/calls/visits to populate this rail.</p>}
</div>
</div>
</div>
<aside className="space-y-5">
<div className="rounded-3xl border border-blue-400/20 bg-blue-500/10 p-5">
<p className="text-xs uppercase tracking-[0.24em] text-blue-300">Next Best Action</p>
<p className="mt-2 text-lg font-semibold text-zinc-100">{fmt(selected.next_best_action ?? profile.recommended_action)}</p>
<p className="mt-3 text-sm leading-6 text-zinc-400">{fmt(profile.rationale)}</p>
</div>
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold text-zinc-100"><Clock className="h-4 w-4" /> Engagement Intelligence</h3>
<FieldRow label="Last Contact" value={shortDate(selected.last_contact_at)} />
<FieldRow label="Channel" value={selected.last_channel} />
<FieldRow label="Days Since" value={selected.days_since_contact} />
<FieldRow label="Preference" value={selected.communication_preference} />
<FieldRow label="Best Time" value={profile.best_contact_time} />
</div>
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold text-zinc-100"><Zap className="h-4 w-4 text-cyan-300" /> Extracted Facts</h3>
{(detail?.extracted_facts ?? []).slice(0, 10).map((fact, index) => (
<div key={String(fact.fact_id ?? index)} className="mb-2 rounded-2xl bg-black/25 p-3 text-xs">
<p className="font-semibold text-cyan-200">{fmt(fact.fact_type)}</p>
<p className="mt-1 text-zinc-400">{fmt(fact.fact_value)}</p>
</div>
))}
{!(detail?.extracted_facts ?? []).length && <p className="text-sm text-zinc-500">No extracted facts loaded.</p>}
</div>
<div className="rounded-3xl border border-white/10 bg-white/[0.03] p-5">
<h3 className="mb-3 text-base font-semibold text-zinc-100">Open Opportunities</h3>
{(detail?.opportunities ?? []).slice(0, 8).map((opp, index) => (
<div key={String(opp.opportunity_id ?? index)} className="mb-2 flex items-center justify-between rounded-2xl bg-black/25 p-3 text-xs">
<span className="text-zinc-200">{fmt(opp.project_name ?? opp.stage)}</span>
<ArrowRight className="h-3.5 w-3.5 text-zinc-500" />
</div>
))}
{!(detail?.opportunities ?? []).length && <p className="text-sm text-zinc-500">No opportunities loaded.</p>}
</div>
</aside>
</div>
</section>
</div>
)}
</main>
</div>
);
}

View File

@@ -215,3 +215,44 @@ export interface KanbanCard {
urgency: string | null;
intent_score: number;
}
export interface OracleClientDataListItem {
person_id: string;
full_name: string;
primary_email: string | null;
primary_phone: string | null;
buyer_type: string | null;
broker_name: string | null;
communication_preference: string | null;
best_contact_time: string | null;
lead_id: string | null;
lead_status: string | null;
budget_band: string | null;
urgency: string | null;
qd_score: number;
last_contact_at: string | null;
last_channel: string | null;
days_since_contact: number | null;
next_best_action: string | null;
next_action_priority: string | null;
projects: string;
interest_count: number;
}
export interface OracleClientTimelineItem {
id: string;
type: string;
title: string | null;
summary: string | null;
date: string | null;
actor: string | null;
}
export interface OracleClientDataDetail {
profile: Record<string, unknown>;
property_interests: Array<Record<string, unknown>>;
opportunities: Array<Record<string, unknown>>;
extracted_facts: Array<Record<string, unknown>>;
qd_scores: Array<Record<string, unknown>>;
timeline: OracleClientTimelineItem[];
}

View File

@@ -3,7 +3,7 @@ import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { inspectAttr } from 'kimi-plugin-inspect-react'
const backendProxyTarget = process.env.VITE_BACKEND_PROXY_TARGET?.trim() || "https://velocity.desineuron.in"
const backendProxyTarget = process.env.VITE_BACKEND_PROXY_TARGET?.trim() || "http://127.0.0.1:8001"
// https://vite.dev/config/
export default defineConfig({