test(frontend): UIUX E2E 测试 — 新增路由导航/创始人端/Admin端/工作台/移动端适配
新增 6 个 E2E 测试文件,覆盖 2-task-uiux.md 全部 50 项任务: - uiux-navigation.spec.ts: 8 tests (today/compare/threads/workspace/ooda/ai-plus/profiles) - sidebar-navigation.spec.ts: 6 tests (投资人 Sidebar 6 业务域) - founder-uiux.spec.ts: 7 tests (创始人端 6 域导航) - admin-uiux.spec.ts: 3 tests (Admin 6 管理域 + 商业秘密保护) - workbench-uiux.spec.ts: 5 tests (Highlights/WorkMode/Tab/InsightRail) - mobile-uiux.spec.ts: 5 tests (移动端抽屉导航 + 创始人底部导航) 同时修复全部 no-explicit-any 警告,替换为 TypeScript 接口定义。 测试结果: 41 E2E passed, 405 backend passed
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { BookOpen, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { listAARs, generateAAR } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { PageContainer, Card} from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** AAR 复盘记录项。 */
|
||||
interface AARItem {
|
||||
trigger_event: string;
|
||||
gap_analysis?: string;
|
||||
lessons?: string[];
|
||||
}
|
||||
|
||||
export default function AARsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<AARItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [triggerEvent, setTriggerEvent] = useState("");
|
||||
const [originalPlan, setOriginalPlan] = useState("");
|
||||
@@ -17,7 +24,7 @@ export default function AARsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
listAARs()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as AARItem[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -29,7 +36,7 @@ export default function AARsPage() {
|
||||
}
|
||||
try {
|
||||
const resp = await generateAAR(triggerEvent, originalPlan, actualResult);
|
||||
setItems([resp.data as Record<string, any>, ...items]);
|
||||
setItems([resp.data as AARItem, ...items]);
|
||||
toast.success("AAR 复盘已生成");
|
||||
} catch {
|
||||
toast.error("生成失败");
|
||||
@@ -77,11 +84,11 @@ export default function AARsPage() {
|
||||
<div className="space-y-3">
|
||||
{items.map((item, i) => (
|
||||
<Card key={i}>
|
||||
<h3 className="font-medium text-gray-900">{item.trigger_event as string}</h3>
|
||||
{item.gap_analysis && <p className="mt-1 text-sm text-gray-600">{item.gap_analysis as string}</p>}
|
||||
<h3 className="font-medium text-gray-900">{item.trigger_event}</h3>
|
||||
{item.gap_analysis && <p className="mt-1 text-sm text-gray-600">{item.gap_analysis}</p>}
|
||||
{Array.isArray(item.lessons) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{(item.lessons as string[]).map((lesson, li) => (
|
||||
{item.lessons.map((lesson, li) => (
|
||||
<p key={li} className="text-xs text-gray-500">• {lesson}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Bot } from "lucide-react";
|
||||
import { listAgentExecutions, reviewAgentExecution } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { PageContainer, Badge} from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** Agent 执行记录项。 */
|
||||
interface AgentExecution {
|
||||
id: string;
|
||||
agent_name: string;
|
||||
autonomy_level: string;
|
||||
output_summary: string;
|
||||
duration_ms?: number;
|
||||
review_status: string;
|
||||
}
|
||||
|
||||
export default function AgentsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<AgentExecution[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const load = () => {
|
||||
listAgentExecutions()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as AgentExecution[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
@@ -53,26 +62,26 @@ export default function AgentsPage() {
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-2 text-gray-900">{item.agent_name as string}</td>
|
||||
<td className="px-4 py-2"><Badge color={item.autonomy_level === "L4" ? "red" : item.autonomy_level === "L3" ? "amber" : "blue"}>{item.autonomy_level as string}</Badge></td>
|
||||
<td className="px-4 py-2 text-gray-600 max-w-xs truncate">{item.output_summary as string}</td>
|
||||
<td className="px-4 py-2 text-gray-900">{item.agent_name}</td>
|
||||
<td className="px-4 py-2"><Badge color={item.autonomy_level === "L4" ? "red" : item.autonomy_level === "L3" ? "amber" : "blue"}>{item.autonomy_level}</Badge></td>
|
||||
<td className="px-4 py-2 text-gray-600 max-w-xs truncate">{item.output_summary}</td>
|
||||
<td className="px-4 py-2 text-gray-500">{item.duration_ms ? `${item.duration_ms}ms` : "-"}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color={item.review_status === "approved" ? "green" : item.review_status === "rejected" ? "red" : "amber"}>
|
||||
{item.review_status as string}
|
||||
{item.review_status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{item.review_status === "pending" && (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleReview(item.id as string, "approved")}
|
||||
onClick={() => handleReview(item.id, "approved")}
|
||||
className="rounded bg-emerald-600 px-2 py-0.5 text-xs text-white hover:bg-emerald-700"
|
||||
>
|
||||
批准
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReview(item.id as string, "rejected")}
|
||||
onClick={() => handleReview(item.id, "rejected")}
|
||||
className="rounded bg-rose-600 px-2 py-0.5 text-xs text-white hover:bg-rose-700"
|
||||
>
|
||||
拒绝
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ScrollText, Plus } from "lucide-react";
|
||||
import {Plus } from "lucide-react";
|
||||
import { listAgreements } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 投资协议项。 */
|
||||
interface Agreement {
|
||||
title: string;
|
||||
signed_at: string;
|
||||
status: string;
|
||||
key_clauses?: unknown[];
|
||||
}
|
||||
|
||||
export default function AgreementsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<Agreement[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listAgreements()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as Agreement[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -49,15 +57,15 @@ export default function AgreementsPage() {
|
||||
) : (
|
||||
items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-2 text-gray-900">{item.title as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.signed_at as string}</td>
|
||||
<td className="px-4 py-2 text-gray-900">{item.title}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.signed_at}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color={item.status === "active" ? "green" : "gray"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{Array.isArray(item.key_clauses) ? (item.key_clauses as unknown[]).length : 0} 条
|
||||
{Array.isArray(item.key_clauses) ? item.key_clauses.length : 0} 条
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { TrendingUp, Plus } from "lucide-react";
|
||||
import {Plus } from "lucide-react";
|
||||
import { listInterventions } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 干预记录项。 */
|
||||
interface Intervention {
|
||||
intervention_type: string;
|
||||
title: string;
|
||||
executed_at: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function AlphaPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<Intervention[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listInterventions()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as Intervention[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -38,12 +46,12 @@ export default function AlphaPage() {
|
||||
<Card key={i}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="blue">{item.intervention_type as string}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title as string}</h3>
|
||||
<Badge color="blue">{item.intervention_type}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">{item.executed_at as string}</span>
|
||||
<span className="text-xs text-gray-400">{item.executed_at}</span>
|
||||
</div>
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description as string}</p>}
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description}</p>}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Users } from "lucide-react";
|
||||
import { listBoardMeetings } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { PageContainer, Badge} from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 董事会会议项。 */
|
||||
interface BoardMeeting {
|
||||
title: string;
|
||||
meeting_at: string;
|
||||
status: string;
|
||||
resolutions?: unknown[];
|
||||
}
|
||||
|
||||
export default function BoardPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<BoardMeeting[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listBoardMeetings()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as BoardMeeting[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -38,15 +45,15 @@ export default function BoardPage() {
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-2 text-gray-900">{item.title as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.meeting_at as string}</td>
|
||||
<td className="px-4 py-2 text-gray-900">{item.title}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.meeting_at}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color={item.status === "completed" ? "green" : item.status === "in_progress" ? "amber" : "blue"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{Array.isArray(item.resolutions) ? (item.resolutions as unknown[]).length : 0}
|
||||
{Array.isArray(item.resolutions) ? item.resolutions.length : 0}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -12,12 +12,74 @@ import { InsightRail } from "@/components/shared/InsightRail";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { AlertTriangle, FileText, ClipboardList, ScrollText, Network, DollarSign, Activity, Users, Bot } from "lucide-react";
|
||||
|
||||
/** 企业详情数据结构。 */
|
||||
interface CompanyDetail {
|
||||
company: { id: string; name: string; industry?: string };
|
||||
health_score: HealthScore | null;
|
||||
recent_reports: ReportItem[];
|
||||
open_risks: RiskItem[];
|
||||
recent_weak_signals: WeakSignalItem[];
|
||||
active_agreements: AgreementItem[];
|
||||
recent_board_meetings: BoardMeetingItem[];
|
||||
latest_financial?: Record<string, unknown>;
|
||||
financial_data_count?: number;
|
||||
}
|
||||
|
||||
/** 健康度评分结构。 */
|
||||
type HealthScore = {
|
||||
total_score: number;
|
||||
trend: string;
|
||||
runway_months: number;
|
||||
ai_commercial_score: number | null;
|
||||
ai_cost_score: number | null;
|
||||
ai_model_product_score: number | null;
|
||||
data_compliance_score: number | null;
|
||||
} & Record<string, number | null | undefined>;
|
||||
|
||||
/** 月报项。 */
|
||||
interface ReportItem {
|
||||
id: string;
|
||||
period_year: number;
|
||||
period_month: number;
|
||||
status: string;
|
||||
ai_summary?: string;
|
||||
}
|
||||
|
||||
/** 风险项。 */
|
||||
interface RiskItem {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
/** 弱信号项。 */
|
||||
interface WeakSignalItem {
|
||||
id: string;
|
||||
content: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
/** 协议项。 */
|
||||
interface AgreementItem {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 董事会会议项。 */
|
||||
interface BoardMeetingItem {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 企业详情工作台 — 多 Tab 集成页面。 */
|
||||
export default function WorkbenchPage() {
|
||||
const params = useParams();
|
||||
const companyId = params.id as string;
|
||||
const [activeTab, setActiveTab] = useState<TabId>("overview");
|
||||
const [detail, setDetail] = useState<any>(null);
|
||||
const [detail, setDetail] = useState<CompanyDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [workMode, setWorkMode] = useState<WorkMode>("overview");
|
||||
|
||||
@@ -25,7 +87,7 @@ export default function WorkbenchPage() {
|
||||
if (!companyId) return;
|
||||
apiFetch(`/companies/${companyId}/detail`)
|
||||
.then((res) => {
|
||||
setDetail(res.data);
|
||||
setDetail(res.data as CompanyDetail);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
@@ -45,8 +107,8 @@ export default function WorkbenchPage() {
|
||||
|
||||
const { company, health_score, recent_reports, open_risks, recent_weak_signals, active_agreements, recent_board_meetings } = detail;
|
||||
|
||||
const highRisksCount = (open_risks || []).filter((r: any) => r.severity === "high" || r.severity === "critical").length;
|
||||
const pendingTasksCount = (recent_reports || []).filter((r: any) => r.status === "pending" || r.status === "draft").length;
|
||||
const highRisksCount = (open_risks || []).filter((r) => r.severity === "high" || r.severity === "critical").length;
|
||||
const pendingTasksCount = (recent_reports || []).filter((r) => r.status === "pending" || r.status === "draft").length;
|
||||
const runway = health_score?.runway_months ?? 12;
|
||||
|
||||
return (
|
||||
@@ -55,7 +117,7 @@ export default function WorkbenchPage() {
|
||||
<HighlightsPanel
|
||||
companyName={company.name}
|
||||
healthScore={health_score?.total_score ?? 0}
|
||||
healthTrend={health_score?.trend ?? "stable"}
|
||||
healthTrend={(health_score?.trend as "up" | "down" | "stable") ?? "stable"}
|
||||
runway={runway}
|
||||
highRisks={highRisksCount}
|
||||
pendingTasks={pendingTasksCount}
|
||||
@@ -75,13 +137,13 @@ export default function WorkbenchPage() {
|
||||
{activeTab === "overview" && <OverviewTab detail={detail} />}
|
||||
{activeTab === "financial" && <FinancialTab detail={detail} />}
|
||||
{activeTab === "operational" && <OperationalTab detail={detail} />}
|
||||
{activeTab === "org" && <OrgTab detail={detail} />}
|
||||
{activeTab === "org" && <OrgTab />}
|
||||
{activeTab === "ai" && <AITab detail={detail} />}
|
||||
{activeTab === "risk" && <RiskTab risks={open_risks} signals={recent_weak_signals} />}
|
||||
{activeTab === "reports" && <ReportsTab reports={recent_reports} />}
|
||||
{activeTab === "board" && <BoardTab meetings={recent_board_meetings} />}
|
||||
{activeTab === "agreements" && <AgreementsTab agreements={active_agreements} />}
|
||||
{activeTab === "synergy" && <SynergyTab companyId={company.id} />}
|
||||
{activeTab === "synergy" && <SynergyTab />}
|
||||
</TabPanel>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +155,7 @@ export default function WorkbenchPage() {
|
||||
}
|
||||
|
||||
/** 概览 Tab — 健康度雷达图 + 维度详情。 */
|
||||
function OverviewTab({ detail }: { detail: any }) {
|
||||
function OverviewTab({ detail }: { detail: CompanyDetail }) {
|
||||
const { health_score } = detail;
|
||||
if (!health_score) {
|
||||
return <EmptyState title="暂无健康度评分数据" />;
|
||||
@@ -113,7 +175,7 @@ function OverviewTab({ detail }: { detail: any }) {
|
||||
}
|
||||
|
||||
/** 财务 Tab。 */
|
||||
function FinancialTab({ detail }: { detail: any }) {
|
||||
function FinancialTab({ detail }: { detail: CompanyDetail }) {
|
||||
const { latest_financial, financial_data_count } = detail;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -134,7 +196,7 @@ function FinancialTab({ detail }: { detail: any }) {
|
||||
}
|
||||
|
||||
/** 经营 Tab。 */
|
||||
function OperationalTab({ detail }: { detail: any }) {
|
||||
function OperationalTab({ detail }: { detail: CompanyDetail }) {
|
||||
const { recent_reports } = detail;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -145,7 +207,7 @@ function OperationalTab({ detail }: { detail: any }) {
|
||||
</div>
|
||||
{recent_reports && recent_reports.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{recent_reports.map((r: any) => (
|
||||
{recent_reports.map((r) => (
|
||||
<div key={r.id} className="flex items-center justify-between text-sm">
|
||||
<span>{r.period_year}年{r.period_month}月</span>
|
||||
<span className="text-muted-foreground">{r.status}</span>
|
||||
@@ -161,7 +223,7 @@ function OperationalTab({ detail }: { detail: any }) {
|
||||
}
|
||||
|
||||
/** 组织 Tab。 */
|
||||
function OrgTab({ detail }: { detail: any }) {
|
||||
function OrgTab() {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -174,7 +236,7 @@ function OrgTab({ detail }: { detail: any }) {
|
||||
}
|
||||
|
||||
/** AI+专项 Tab。 */
|
||||
function AITab({ detail }: { detail: any }) {
|
||||
function AITab({ detail }: { detail: CompanyDetail }) {
|
||||
const { health_score } = detail;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -197,7 +259,7 @@ function AITab({ detail }: { detail: any }) {
|
||||
}
|
||||
|
||||
/** 风险 Tab。 */
|
||||
function RiskTab({ risks, signals }: { risks: any[]; signals: any[] }) {
|
||||
function RiskTab({ risks, signals }: { risks: RiskItem[]; signals: WeakSignalItem[] }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
@@ -207,7 +269,7 @@ function RiskTab({ risks, signals }: { risks: any[]; signals: any[] }) {
|
||||
</div>
|
||||
{risks && risks.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{risks.map((r: any) => (
|
||||
{risks.map((r) => (
|
||||
<div key={r.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{r.title}</span>
|
||||
@@ -229,7 +291,7 @@ function RiskTab({ risks, signals }: { risks: any[]; signals: any[] }) {
|
||||
<h3 className="text-sm font-medium">弱信号 ({signals?.length || 0})</h3>
|
||||
{signals && signals.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{signals.map((s: any) => (
|
||||
{signals.map((s) => (
|
||||
<div key={s.id} className="flex items-center justify-between text-sm">
|
||||
<span className="truncate">{s.content}</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-muted-foreground">
|
||||
@@ -247,7 +309,7 @@ function RiskTab({ risks, signals }: { risks: any[]; signals: any[] }) {
|
||||
}
|
||||
|
||||
/** 月报 Tab。 */
|
||||
function ReportsTab({ reports }: { reports: any[] }) {
|
||||
function ReportsTab({ reports }: { reports: ReportItem[] }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -256,7 +318,7 @@ function ReportsTab({ reports }: { reports: any[] }) {
|
||||
</div>
|
||||
{reports && reports.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{reports.map((r: any) => (
|
||||
{reports.map((r) => (
|
||||
<div key={r.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{r.period_year}年{r.period_month}月</span>
|
||||
@@ -274,7 +336,7 @@ function ReportsTab({ reports }: { reports: any[] }) {
|
||||
}
|
||||
|
||||
/** 董事会 Tab。 */
|
||||
function BoardTab({ meetings }: { meetings: any[] }) {
|
||||
function BoardTab({ meetings }: { meetings: BoardMeetingItem[] }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -283,7 +345,7 @@ function BoardTab({ meetings }: { meetings: any[] }) {
|
||||
</div>
|
||||
{meetings && meetings.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{meetings.map((m: any) => (
|
||||
{meetings.map((m) => (
|
||||
<div key={m.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
|
||||
<span className="text-sm font-medium">{m.title}</span>
|
||||
<span className="rounded bg-muted px-2 py-0.5 text-xs">{m.status}</span>
|
||||
@@ -298,7 +360,7 @@ function BoardTab({ meetings }: { meetings: any[] }) {
|
||||
}
|
||||
|
||||
/** 协议 Tab。 */
|
||||
function AgreementsTab({ agreements }: { agreements: any[] }) {
|
||||
function AgreementsTab({ agreements }: { agreements: AgreementItem[] }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -307,7 +369,7 @@ function AgreementsTab({ agreements }: { agreements: any[] }) {
|
||||
</div>
|
||||
{agreements && agreements.length > 0 ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{agreements.map((a: any) => (
|
||||
{agreements.map((a) => (
|
||||
<div key={a.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
|
||||
<span className="text-sm font-medium">{a.title}</span>
|
||||
<span className="rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-700">{a.status}</span>
|
||||
@@ -322,7 +384,7 @@ function AgreementsTab({ agreements }: { agreements: any[] }) {
|
||||
}
|
||||
|
||||
/** 协同 Tab。 */
|
||||
function SynergyTab({ companyId }: { companyId: string }) {
|
||||
function SynergyTab() {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function CompaniesPage() {
|
||||
}, [page, keyword]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadCompanies();
|
||||
}, [loadCompanies]);
|
||||
|
||||
|
||||
@@ -8,21 +8,25 @@ import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { Sparkles, Plus } from "lucide-react";
|
||||
|
||||
/** 客户增长引擎页面。 */
|
||||
|
||||
/** 客户获取方案项。 */
|
||||
interface CustomerPlan {
|
||||
id: string;
|
||||
execution_status?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export default function CustomerGrowthPage() {
|
||||
const [plans, setPlans] = useState<any[]>([]);
|
||||
const [plans, setPlans] = useState<CustomerPlan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [companyContext, setCompanyContext] = useState("");
|
||||
const [lpResources, setLpResources] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
}, []);
|
||||
|
||||
async function loadPlans() {
|
||||
try {
|
||||
const res = await apiFetch<any[]>("/customer-plans");
|
||||
setPlans((res.data as any[]) || []);
|
||||
const res = await apiFetch<CustomerPlan[]>("/customer-plans");
|
||||
setPlans((res.data as CustomerPlan[]) || []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -30,16 +34,21 @@ export default function CustomerGrowthPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadPlans();
|
||||
}, []);
|
||||
|
||||
async function handleGenerate() {
|
||||
if (!companyContext.trim()) return;
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/customer-plans/generate", {
|
||||
const res = await apiFetch<CustomerPlan>("/customer-plans/generate", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ company_context: companyContext, lp_resources: lpResources }),
|
||||
});
|
||||
if (res.data) {
|
||||
setPlans((prev) => [{ ...res.data, id: Date.now().toString(), execution_status: "planned" }, ...prev]);
|
||||
setPlans((prev) => [{ ...res.data, id: Date.now().toString(), execution_status: "planned" } as CustomerPlan, ...prev]);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
@@ -4,27 +4,45 @@ import { useEffect, useState } from "react";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { FileText, TrendingUp, AlertTriangle, Sparkles } from "lucide-react";
|
||||
import { FileText, TrendingUp, AlertTriangle} from "lucide-react";
|
||||
|
||||
/** 流失风险项。 */
|
||||
interface ChurnRisk {
|
||||
company_name?: string;
|
||||
company_id?: string;
|
||||
signals?: string;
|
||||
risk_level?: string;
|
||||
}
|
||||
|
||||
/** QBR 结果。 */
|
||||
interface QbrResult {
|
||||
summary?: string;
|
||||
key_metrics?: string[];
|
||||
next_quarter_recommendations?: string[];
|
||||
}
|
||||
|
||||
/** 扩展机会项。 */
|
||||
interface ExpansionOpportunity {
|
||||
type?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 客户成功运营页面 — QBR + 扩展机会 + 流失风险。 */
|
||||
export default function CustomerSuccessPage() {
|
||||
const [churnRisks, setChurnRisks] = useState<any[]>([]);
|
||||
const [churnRisks, setChurnRisks] = useState<ChurnRisk[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [qbrData, setQbrData] = useState("");
|
||||
const [qbrResult, setQbrResult] = useState<any>(null);
|
||||
const [qbrResult, setQbrResult] = useState<QbrResult | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [expansionData, setExpansionData] = useState("");
|
||||
const [expansionResult, setExpansionResult] = useState<any>(null);
|
||||
const [expansionResult, setExpansionResult] = useState<ExpansionOpportunity[] | null>(null);
|
||||
const [expanding, setExpanding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadChurnRisks();
|
||||
}, []);
|
||||
|
||||
async function loadChurnRisks() {
|
||||
try {
|
||||
const res = await apiFetch<any[]>("/customer-success/churn-risk");
|
||||
setChurnRisks((res.data as any[]) || []);
|
||||
const res = await apiFetch<ChurnRisk[]>("/customer-success/churn-risk");
|
||||
setChurnRisks((res.data as ChurnRisk[]) || []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -32,11 +50,16 @@ export default function CustomerSuccessPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadChurnRisks();
|
||||
}, []);
|
||||
|
||||
async function handleQBR() {
|
||||
if (!qbrData.trim()) return;
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/customer-success/qbr", {
|
||||
const res = await apiFetch<QbrResult>("/customer-success/qbr", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ company_id: "", quarter_data: qbrData }),
|
||||
});
|
||||
@@ -52,7 +75,7 @@ export default function CustomerSuccessPage() {
|
||||
if (!expansionData.trim()) return;
|
||||
setExpanding(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/customer-success/expansion", {
|
||||
const res = await apiFetch<ExpansionOpportunity[]>("/customer-success/expansion", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ company_data: expansionData }),
|
||||
});
|
||||
@@ -145,7 +168,7 @@ export default function CustomerSuccessPage() {
|
||||
</button>
|
||||
{expansionResult && Array.isArray(expansionResult) && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{expansionResult.map((opp: any, i: number) => (
|
||||
{expansionResult.map((opp, i: number) => (
|
||||
<div key={i} className="rounded-md border border-[var(--border)] px-3 py-2 text-sm">
|
||||
<div className="font-medium">{opp.type || opp.title || `机会 ${i + 1}`}</div>
|
||||
{opp.description && <div className="mt-1 text-xs text-muted-foreground">{opp.description}</div>}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Bot, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { listDigitalTwins, buildDigitalTwin, simulateTwin } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 数字孪生模型项。 */
|
||||
interface DigitalTwinItem {
|
||||
accuracy_score?: number;
|
||||
}
|
||||
|
||||
/** 模拟结果。 */
|
||||
interface SimulationResult {
|
||||
projected_outcome: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export default function DigitalTwinsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<DigitalTwinItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [companyData, setCompanyData] = useState("");
|
||||
const [scenario, setScenario] = useState("");
|
||||
const [simResult, setSimResult] = useState<Record<string, any> | null>(null);
|
||||
const [simResult, setSimResult] = useState<SimulationResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listDigitalTwins("")
|
||||
@@ -29,8 +40,8 @@ export default function DigitalTwinsPage() {
|
||||
}
|
||||
try {
|
||||
const resp = await buildDigitalTwin(companyData);
|
||||
toast.success("数字孪生模型已构建");
|
||||
setItems([resp.data as Record<string, any>, ...items]);
|
||||
toast.success("数字孿生模型已构建");
|
||||
setItems([resp.data as DigitalTwinItem, ...items]);
|
||||
} catch {
|
||||
toast.error("构建失败");
|
||||
}
|
||||
@@ -43,7 +54,7 @@ export default function DigitalTwinsPage() {
|
||||
}
|
||||
try {
|
||||
const resp = await simulateTwin({}, scenario);
|
||||
setSimResult(resp.data as Record<string, any>);
|
||||
setSimResult(resp.data as SimulationResult);
|
||||
} catch {
|
||||
toast.error("模拟失败");
|
||||
}
|
||||
@@ -85,9 +96,9 @@ export default function DigitalTwinsPage() {
|
||||
</button>
|
||||
{simResult && (
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
<p>预测结果:{simResult.projected_outcome as string}</p>
|
||||
<p>预测结果:{simResult.projected_outcome}</p>
|
||||
{simResult.confidence != null && (
|
||||
<p className="mt-1">置信度:<Badge color="blue">{((simResult.confidence as number) * 100).toFixed(0)}%</Badge></p>
|
||||
<p className="mt-1">置信度:<Badge color="blue">{(simResult.confidence * 100).toFixed(0)}%</Badge></p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -104,8 +115,8 @@ export default function DigitalTwinsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-gray-900">数字孪生模型</h3>
|
||||
{item.accuracy_score != null && (
|
||||
<Badge color={(item.accuracy_score as number) > 0.7 ? "green" : "amber"}>
|
||||
精度 {((item.accuracy_score as number) * 100).toFixed(0)}%
|
||||
<Badge color={item.accuracy_score > 0.7 ? "green" : "amber"}>
|
||||
精度 {(item.accuracy_score * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { listMajorEvents, listInquiries } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 重大事项项。 */
|
||||
interface MajorEvent {
|
||||
event_type: string;
|
||||
title: string;
|
||||
severity: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 追问清单项。 */
|
||||
interface Inquiry {
|
||||
status: string;
|
||||
questions?: unknown[];
|
||||
}
|
||||
|
||||
export default function EventsPage() {
|
||||
const [events, setEvents] = useState<Record<string, any>[]>([]);
|
||||
const [inquiries, setInquiries] = useState<Record<string, any>[]>([]);
|
||||
const [events, setEvents] = useState<MajorEvent[]>([]);
|
||||
const [inquiries, setInquiries] = useState<Inquiry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [tab, setTab] = useState<"events" | "inquiries">("events");
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([listMajorEvents(), listInquiries()])
|
||||
.then(([e, i]) => {
|
||||
setEvents((e.data as Record<string, any>[]) ?? []);
|
||||
setInquiries((i.data as Record<string, any>[]) ?? []);
|
||||
setEvents((e.data as MajorEvent[]) ?? []);
|
||||
setInquiries((i.data as Inquiry[]) ?? []);
|
||||
})
|
||||
.catch(() => {
|
||||
setEvents([]);
|
||||
@@ -54,14 +67,14 @@ export default function EventsPage() {
|
||||
<div key={i} className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="blue">{item.event_type as string}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title as string}</h3>
|
||||
<Badge color="blue">{item.event_type}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
</div>
|
||||
<Badge color={item.severity === "critical" ? "red" : item.severity === "high" ? "amber" : "gray"}>
|
||||
{item.severity as string}
|
||||
{item.severity}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description as string}</p>}
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -76,7 +89,7 @@ export default function EventsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-gray-900">追问清单</h3>
|
||||
<Badge color={item.status === "answered" ? "green" : item.status === "closed" ? "gray" : "amber"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { TrendingUp, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { listExitPredictions, predictExit } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 退出预测项。 */
|
||||
interface ExitPrediction {
|
||||
exit_path?: string;
|
||||
expected_return?: string;
|
||||
confidence?: number;
|
||||
recommendation?: string;
|
||||
}
|
||||
|
||||
export default function ExitSignalsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<ExitPrediction[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [companyData, setCompanyData] = useState("");
|
||||
const [predicting, setPredicting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
listExitPredictions()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as ExitPrediction[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -29,7 +37,7 @@ export default function ExitSignalsPage() {
|
||||
setPredicting(true);
|
||||
try {
|
||||
const resp = await predictExit(companyData);
|
||||
const result = resp.data as Record<string, any>;
|
||||
const result = resp.data as ExitPrediction;
|
||||
if (result) {
|
||||
setItems([result, ...items]);
|
||||
toast.success("预测完成");
|
||||
@@ -70,16 +78,16 @@ export default function ExitSignalsPage() {
|
||||
<Card key={i}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.exit_path && <Badge color="blue">{item.exit_path as string}</Badge>}
|
||||
<span className="text-sm text-gray-900">期望收益:{item.expected_return as string ?? "-"}</span>
|
||||
{item.exit_path && <Badge color="blue">{item.exit_path}</Badge>}
|
||||
<span className="text-sm text-gray-900">期望收益:{item.expected_return ?? "-"}</span>
|
||||
</div>
|
||||
{item.confidence != null && (
|
||||
<Badge color={(item.confidence as number) > 0.6 ? "green" : "amber"}>
|
||||
置信度 {((item.confidence as number) * 100).toFixed(0)}%
|
||||
<Badge color={item.confidence > 0.6 ? "green" : "amber"}>
|
||||
置信度 {(item.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{item.recommendation && <p className="mt-2 text-sm text-gray-600">{item.recommendation as string}</p>}
|
||||
{item.recommendation && <p className="mt-2 text-sm text-gray-600">{item.recommendation}</p>}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Receipt } from "lucide-react";
|
||||
import { listFinancialData, validateFinancial } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { listFinancialData} from "@/lib/api-v2";
|
||||
import { PageContainer,Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 财务数据项。 */
|
||||
interface FinancialData {
|
||||
period_year: number;
|
||||
period_month: number;
|
||||
statement_type: string;
|
||||
credibility_score: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 投资人端 — 财务数据校验页面。
|
||||
*/
|
||||
export default function FinancialPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<FinancialData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [companyId, setCompanyId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!companyId) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
listFinancialData(companyId)
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as FinancialData[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [companyId]);
|
||||
@@ -62,16 +71,16 @@ export default function FinancialPage() {
|
||||
<td className="px-4 py-2 text-gray-900">
|
||||
{item.period_year}-{String(item.period_month).padStart(2, "0")}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.statement_type as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.statement_type}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color={
|
||||
(item.credibility_score as number) >= 80 ? "green" :
|
||||
(item.credibility_score as number) >= 50 ? "amber" : "red"
|
||||
item.credibility_score >= 80 ? "green" :
|
||||
item.credibility_score >= 50 ? "amber" : "red"
|
||||
}>
|
||||
{item.credibility_score as number}
|
||||
{item.credibility_score}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">{item.source as string}</td>
|
||||
<td className="px-4 py-2 text-gray-500">{item.source}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Lightbulb, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { discoverInnovation } from "@/lib/api-v2";
|
||||
import { PageContainer, Card } from "@/components/shared/PageContainer";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 创新机会项。 */
|
||||
interface InnovationResult {
|
||||
title: string;
|
||||
combined_capability: string;
|
||||
market_analysis?: string;
|
||||
}
|
||||
|
||||
export default function InnovationPage() {
|
||||
const [capabilities, setCapabilities] = useState("");
|
||||
const [results, setResults] = useState<Record<string, any>[]>([]);
|
||||
const [results, setResults] = useState<InnovationResult[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleDiscover = async () => {
|
||||
@@ -20,7 +27,7 @@ export default function InnovationPage() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await discoverInnovation(capabilities);
|
||||
setResults((resp.data as Record<string, any>[]) ?? []);
|
||||
setResults((resp.data as InnovationResult[]) ?? []);
|
||||
} catch {
|
||||
toast.error("分析失败");
|
||||
} finally {
|
||||
@@ -51,9 +58,9 @@ export default function InnovationPage() {
|
||||
<div className="space-y-3">
|
||||
{results.map((item, i) => (
|
||||
<Card key={i}>
|
||||
<h3 className="font-medium text-gray-900">{item.title as string}</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">{item.combined_capability as string}</p>
|
||||
{item.market_analysis && <p className="mt-1 text-xs text-gray-400">{item.market_analysis as string}</p>}
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">{item.combined_capability}</p>
|
||||
{item.market_analysis && <p className="mt-1 text-xs text-gray-400">{item.market_analysis}</p>}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Network, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { buildKnowledgeGraph, matchBestStrategy } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 知识图谱数据。 */
|
||||
interface KnowledgeGraphData {
|
||||
nodes?: unknown[];
|
||||
relations?: unknown[];
|
||||
}
|
||||
|
||||
/** 策略匹配结果。 */
|
||||
interface StrategyMatchResult {
|
||||
recommended_strategy: string;
|
||||
match_confidence?: number;
|
||||
}
|
||||
|
||||
export default function KnowledgeGraphPage() {
|
||||
const [experiences, setExperiences] = useState("");
|
||||
const [graph, setGraph] = useState<Record<string, any> | null>(null);
|
||||
const [graph, setGraph] = useState<KnowledgeGraphData | null>(null);
|
||||
const [companyProfile, setCompanyProfile] = useState("");
|
||||
const [matchResult, setMatchResult] = useState<Record<string, any> | null>(null);
|
||||
const [matchResult, setMatchResult] = useState<StrategyMatchResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleBuild = async () => {
|
||||
@@ -21,7 +33,7 @@ export default function KnowledgeGraphPage() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await buildKnowledgeGraph(experiences);
|
||||
setGraph(resp.data as Record<string, any>);
|
||||
setGraph(resp.data as KnowledgeGraphData);
|
||||
toast.success("知识图谱已构建");
|
||||
} catch {
|
||||
toast.error("构建失败");
|
||||
@@ -37,7 +49,7 @@ export default function KnowledgeGraphPage() {
|
||||
}
|
||||
try {
|
||||
const resp = await matchBestStrategy(companyProfile, graph);
|
||||
setMatchResult(resp.data as Record<string, any>);
|
||||
setMatchResult(resp.data as StrategyMatchResult);
|
||||
} catch {
|
||||
toast.error("匹配失败");
|
||||
}
|
||||
@@ -63,8 +75,8 @@ export default function KnowledgeGraphPage() {
|
||||
</button>
|
||||
{graph && (
|
||||
<div className="mt-3 text-sm">
|
||||
<p>节点数:{Array.isArray(graph.nodes) ? (graph.nodes as unknown[]).length : 0}</p>
|
||||
<p>关系数:{Array.isArray(graph.relations) ? (graph.relations as unknown[]).length : 0}</p>
|
||||
<p>节点数:{Array.isArray(graph.nodes) ? graph.nodes.length : 0}</p>
|
||||
<p>关系数:{Array.isArray(graph.relations) ? graph.relations.length : 0}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -87,9 +99,9 @@ export default function KnowledgeGraphPage() {
|
||||
</button>
|
||||
{matchResult && (
|
||||
<div className="mt-3 space-y-2 text-sm text-gray-600">
|
||||
<p>推荐策略:{matchResult.recommended_strategy as string}</p>
|
||||
<p>推荐策略:{matchResult.recommended_strategy}</p>
|
||||
{matchResult.match_confidence != null && (
|
||||
<p>匹配置信度:<Badge color="blue">{((matchResult.match_confidence as number) * 100).toFixed(0)}%</Badge></p>
|
||||
<p>匹配置信度:<Badge color="blue">{(matchResult.match_confidence * 100).toFixed(0)}%</Badge></p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { GitBranch } from "lucide-react";
|
||||
import { listMilestones } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 里程碑项。 */
|
||||
interface Milestone {
|
||||
is_current?: boolean;
|
||||
name: string;
|
||||
status: string;
|
||||
description?: string;
|
||||
target_date?: string;
|
||||
}
|
||||
|
||||
export default function MilestonesPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<Milestone[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [companyId, setCompanyId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!companyId) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
listMilestones(companyId)
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as Milestone[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [companyId]);
|
||||
@@ -48,14 +57,14 @@ export default function MilestonesPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.is_current && <Badge color="blue">当前路径</Badge>}
|
||||
<span className="font-medium text-gray-900">{item.name as string}</span>
|
||||
<span className="font-medium text-gray-900">{item.name}</span>
|
||||
</div>
|
||||
<Badge color={item.status === "completed" ? "green" : item.status === "in_progress" ? "amber" : "gray"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.description && <p className="mt-1 text-sm text-gray-600">{item.description as string}</p>}
|
||||
{item.target_date && <p className="mt-1 text-xs text-gray-400">目标日期:{item.target_date as string}</p>}
|
||||
{item.description && <p className="mt-1 text-sm text-gray-600">{item.description}</p>}
|
||||
{item.target_date && <p className="mt-1 text-xs text-gray-400">目标日期:{item.target_date}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -7,21 +7,33 @@ import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { Lightbulb, Sparkles, Check, X, TrendingUp, BarChart3 } from "lucide-react";
|
||||
|
||||
/** 行为助推引擎页面。 */
|
||||
|
||||
/** 助推记录项。 */
|
||||
interface NudgeItem {
|
||||
id: string;
|
||||
nudge_type: string;
|
||||
message: string;
|
||||
accepted: boolean;
|
||||
}
|
||||
|
||||
/** 助推策略结果。 */
|
||||
interface NudgeStrategy {
|
||||
strategy?: string;
|
||||
message?: string;
|
||||
expected_effect?: string;
|
||||
}
|
||||
|
||||
export default function NudgesPage() {
|
||||
const [nudges, setNudges] = useState<any[]>([]);
|
||||
const [nudges, setNudges] = useState<NudgeItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [context, setContext] = useState("");
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [strategy, setStrategy] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadNudges();
|
||||
}, []);
|
||||
const [strategy, setStrategy] = useState<NudgeStrategy | null>(null);
|
||||
|
||||
async function loadNudges() {
|
||||
try {
|
||||
const res = await apiFetch<any[]>("/nudges");
|
||||
setNudges((res.data as any[]) || []);
|
||||
const res = await apiFetch<NudgeItem[]>("/nudges");
|
||||
setNudges((res.data as NudgeItem[]) || []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -29,11 +41,16 @@ export default function NudgesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadNudges();
|
||||
}, []);
|
||||
|
||||
async function handleSelect() {
|
||||
if (!context.trim()) return;
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/nudges/select", {
|
||||
const res = await apiFetch<NudgeStrategy>("/nudges/select", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ context }),
|
||||
});
|
||||
@@ -146,7 +163,7 @@ export default function NudgesPage() {
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<TrendingUp size={12} aria-hidden="true" />
|
||||
采纳率较上月提升 12%,"默认选项"策略效果最佳。疲劳度正常,无需调整推送频率。
|
||||
采纳率较上月提升 12%,“默认选项”策略效果最佳。疲劳度正常,无需调整推送频率。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Target, Plus } from "lucide-react";
|
||||
import {Plus } from "lucide-react";
|
||||
import { listOKRs } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** OKR 关键结果项。 */
|
||||
interface KeyResult {
|
||||
title?: string;
|
||||
objective?: string;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
/** OKR 项。 */
|
||||
interface OKRItem {
|
||||
quarter: string;
|
||||
objective: string;
|
||||
alignment_score?: number;
|
||||
status: string;
|
||||
key_results?: KeyResult[];
|
||||
}
|
||||
|
||||
export default function OKRsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<OKRItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listOKRs()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as OKRItem[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -38,21 +54,21 @@ export default function OKRsPage() {
|
||||
<Card key={i}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Badge color="blue">{item.quarter as string}</Badge>
|
||||
<h3 className="mt-1 font-medium text-gray-900">{item.objective as string}</h3>
|
||||
<Badge color="blue">{item.quarter}</Badge>
|
||||
<h3 className="mt-1 font-medium text-gray-900">{item.objective}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.alignment_score != null && (
|
||||
<Badge color={(item.alignment_score as number) >= 75 ? "green" : "amber"}>
|
||||
<Badge color={item.alignment_score >= 75 ? "green" : "amber"}>
|
||||
对齐度 {item.alignment_score}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge color={item.status === "active" ? "green" : "gray"}>{item.status as string}</Badge>
|
||||
<Badge color={item.status === "active" ? "green" : "gray"}>{item.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{Array.isArray(item.key_results) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{(item.key_results as Record<string, any>[]).map((kr, ki) => (
|
||||
{item.key_results.map((kr, ki) => (
|
||||
<div key={ki} className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-gray-400" />
|
||||
{String(kr.title ?? kr.objective ?? kr)}
|
||||
|
||||
@@ -7,21 +7,41 @@ import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { Users, Sparkles, Circle } from "lucide-react";
|
||||
|
||||
/** Peer Learning Circles 页面。 */
|
||||
|
||||
/** Circle 项。 */
|
||||
interface CircleItem {
|
||||
id: string;
|
||||
topic: string;
|
||||
status: string;
|
||||
description?: string;
|
||||
members?: unknown[];
|
||||
conclusions?: string;
|
||||
action_commitments?: string;
|
||||
}
|
||||
|
||||
/** 匹配结果。 */
|
||||
interface MatchResult {
|
||||
topic?: string;
|
||||
matched_founders?: MatchedFounder[];
|
||||
discussion_framework?: string;
|
||||
}
|
||||
|
||||
/** 匹配的创始人。 */
|
||||
interface MatchedFounder {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function PeerCirclesPage() {
|
||||
const [circles, setCircles] = useState<any[]>([]);
|
||||
const [circles, setCircles] = useState<CircleItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [foundersContext, setFoundersContext] = useState("");
|
||||
const [matching, setMatching] = useState(false);
|
||||
const [matchResult, setMatchResult] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadCircles();
|
||||
}, []);
|
||||
const [matchResult, setMatchResult] = useState<MatchResult | null>(null);
|
||||
|
||||
async function loadCircles() {
|
||||
try {
|
||||
const res = await apiFetch<any[]>("/peer-circles");
|
||||
setCircles((res.data as any[]) || []);
|
||||
const res = await apiFetch<CircleItem[]>("/peer-circles");
|
||||
setCircles((res.data as CircleItem[]) || []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -29,11 +49,16 @@ export default function PeerCirclesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadCircles();
|
||||
}, []);
|
||||
|
||||
async function handleMatch() {
|
||||
if (!foundersContext.trim()) return;
|
||||
setMatching(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/peer-circles/match", {
|
||||
const res = await apiFetch<MatchResult>("/peer-circles/match", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ founders_context: foundersContext }),
|
||||
});
|
||||
@@ -87,7 +112,7 @@ export default function PeerCirclesPage() {
|
||||
<div>
|
||||
<span className="text-muted-foreground">匹配创始人:</span>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{matchResult.matched_founders.map((f: any, i: number) => (
|
||||
{matchResult.matched_founders.map((f, i: number) => (
|
||||
<span key={i} className="rounded bg-[var(--investor-primary)]/10 px-2 py-0.5 text-xs text-[var(--investor-primary)]">
|
||||
{typeof f === "string" ? f : f.name || `创始人${i + 1}`}
|
||||
</span>
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { BarChart3, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { rebalancePortfolio, runMonteCarlo } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 再平衡结果。 */
|
||||
interface RebalanceResult {
|
||||
irr_impact?: number;
|
||||
dpi_impact?: number;
|
||||
}
|
||||
|
||||
/** Monte Carlo 结果。 */
|
||||
interface MonteCarloResult {
|
||||
percentile_p5?: string;
|
||||
percentile_p50?: string;
|
||||
percentile_p95?: string;
|
||||
}
|
||||
|
||||
export default function PortfolioPage() {
|
||||
const [rebalanceResult, setRebalanceResult] = useState<Record<string, any> | null>(null);
|
||||
const [mcResult, setMcResult] = useState<Record<string, any> | null>(null);
|
||||
const [rebalanceResult, setRebalanceResult] = useState<RebalanceResult | null>(null);
|
||||
const [mcResult, setMcResult] = useState<MonteCarloResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleRebalance = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await rebalancePortfolio([]);
|
||||
setRebalanceResult(resp.data as Record<string, any>);
|
||||
setRebalanceResult(resp.data as RebalanceResult);
|
||||
} catch {
|
||||
toast.error("分析失败");
|
||||
} finally {
|
||||
@@ -27,7 +40,7 @@ export default function PortfolioPage() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await runMonteCarlo([0.15, 0.22, 0.08, 0.35, 0.12]);
|
||||
setMcResult(resp.data as Record<string, any>);
|
||||
setMcResult(resp.data as MonteCarloResult);
|
||||
} catch {
|
||||
toast.error("模拟失败");
|
||||
} finally {
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ShieldCheck, Sparkles } from "lucide-react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { listPreMortems, runPreMortem, listRedTeams, runRedTeam } from "@/lib/api-v2";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** Pre-mortem 记录项。 */
|
||||
interface PreMortemItem {
|
||||
decision_context: string;
|
||||
failure_paths?: FailurePath[];
|
||||
}
|
||||
|
||||
/** 失败路径项。 */
|
||||
interface FailurePath {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/** Red Team 记录项。 */
|
||||
interface RedTeamItem {
|
||||
perspective: string;
|
||||
analysis: string;
|
||||
}
|
||||
|
||||
export default function PreMortemsPage() {
|
||||
const [tab, setTab] = useState<"pre-mortem" | "red-team">("pre-mortem");
|
||||
const [preMortems, setPreMortems] = useState<Record<string, any>[]>([]);
|
||||
const [redTeams, setRedTeams] = useState<Record<string, any>[]>([]);
|
||||
const [preMortems, setPreMortems] = useState<PreMortemItem[]>([]);
|
||||
const [redTeams, setRedTeams] = useState<RedTeamItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [input, setInput] = useState("");
|
||||
const [perspective, setPerspective] = useState("competitor");
|
||||
@@ -19,8 +36,8 @@ export default function PreMortemsPage() {
|
||||
useEffect(() => {
|
||||
Promise.all([listPreMortems(), listRedTeams()])
|
||||
.then(([pm, rt]) => {
|
||||
setPreMortems((pm.data as Record<string, any>[]) ?? []);
|
||||
setRedTeams((rt.data as Record<string, any>[]) ?? []);
|
||||
setPreMortems((pm.data as PreMortemItem[]) ?? []);
|
||||
setRedTeams((rt.data as RedTeamItem[]) ?? []);
|
||||
})
|
||||
.catch(() => {
|
||||
setPreMortems([]);
|
||||
@@ -37,11 +54,11 @@ export default function PreMortemsPage() {
|
||||
try {
|
||||
if (tab === "pre-mortem") {
|
||||
const resp = await runPreMortem(input);
|
||||
setPreMortems([resp.data as Record<string, any>, ...preMortems]);
|
||||
setPreMortems([resp.data as PreMortemItem, ...preMortems]);
|
||||
toast.success("Pre-mortem 分析完成");
|
||||
} else {
|
||||
const resp = await runRedTeam(input, perspective);
|
||||
setRedTeams([resp.data as Record<string, any>, ...redTeams]);
|
||||
setRedTeams([resp.data as RedTeamItem, ...redTeams]);
|
||||
toast.success("Red Team 分析完成");
|
||||
}
|
||||
} catch {
|
||||
@@ -103,7 +120,7 @@ export default function PreMortemsPage() {
|
||||
<h3 className="font-medium text-gray-900">{item.decision_context as string}</h3>
|
||||
{Array.isArray(item.failure_paths) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{(item.failure_paths as Record<string, any>[]).map((fp, fi) => (
|
||||
{(item.failure_paths as FailurePath[]).map((fp, fi) => (
|
||||
<p key={fi} className="text-xs text-gray-500">• {String(fp.path ?? fp)}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -7,22 +7,38 @@ import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { Sparkles, Grid3x3 } from "lucide-react";
|
||||
|
||||
/** AI 产品竞争力诊断页面。 */
|
||||
|
||||
/** 诊断维度项。 */
|
||||
interface DiagnosticDimension {
|
||||
name: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
/** 诊断结果。 */
|
||||
interface DiagnosticResult {
|
||||
dimensions?: DiagnosticDimension[];
|
||||
roadmap_suggestions?: string[];
|
||||
}
|
||||
|
||||
/** 历史诊断项。 */
|
||||
interface DiagnosticHistory {
|
||||
id: string;
|
||||
product_name?: string;
|
||||
dimensions?: DiagnosticDimension[];
|
||||
}
|
||||
|
||||
export default function ProductDiagnosticsPage() {
|
||||
const [diagnostics, setDiagnostics] = useState<any[]>([]);
|
||||
const [diagnostics, setDiagnostics] = useState<DiagnosticHistory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [productInfo, setProductInfo] = useState("");
|
||||
const [competitorInfo, setCompetitorInfo] = useState("");
|
||||
const [diagnosing, setDiagnosing] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadDiagnostics();
|
||||
}, []);
|
||||
const [result, setResult] = useState<DiagnosticResult | null>(null);
|
||||
|
||||
async function loadDiagnostics() {
|
||||
try {
|
||||
const res = await apiFetch<any[]>("/product-diagnostics");
|
||||
setDiagnostics((res.data as any[]) || []);
|
||||
const res = await apiFetch<DiagnosticHistory[]>("/product-diagnostics");
|
||||
setDiagnostics((res.data as DiagnosticHistory[]) || []);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -30,11 +46,16 @@ export default function ProductDiagnosticsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadDiagnostics();
|
||||
}, []);
|
||||
|
||||
async function handleDiagnose() {
|
||||
if (!productInfo.trim()) return;
|
||||
setDiagnosing(true);
|
||||
try {
|
||||
const res = await apiFetch<any>("/product-diagnostics/diagnose", {
|
||||
const res = await apiFetch<DiagnosticResult>("/product-diagnostics/diagnose", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ product_info: productInfo, competitor_info: competitorInfo }),
|
||||
});
|
||||
@@ -107,7 +128,7 @@ export default function ProductDiagnosticsPage() {
|
||||
</div>
|
||||
{result.dimensions && Array.isArray(result.dimensions) && (
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 md:grid-cols-3">
|
||||
{result.dimensions.map((dim: any, i: number) => {
|
||||
{result.dimensions.map((dim, i: number) => {
|
||||
const score = dim.score ?? 0;
|
||||
const color = score >= 75 ? "bg-emerald-200" : score >= 50 ? "bg-amber-100" : "bg-rose-200";
|
||||
return (
|
||||
@@ -140,7 +161,7 @@ export default function ProductDiagnosticsPage() {
|
||||
<h3 className="text-sm font-medium">{d.product_name || "未命名产品"}</h3>
|
||||
{d.dimensions && Array.isArray(d.dimensions) && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{d.dimensions.map((dim: any, i: number) => (
|
||||
{d.dimensions.map((dim, i: number) => (
|
||||
<span key={i} className="rounded bg-muted px-2 py-0.5 text-xs">
|
||||
{dim.name}: {dim.score?.toFixed(0) ?? "N/A"}
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { Users, Building2, TrendingUp, AlertTriangle, Target, Lightbulb } from "lucide-react";
|
||||
import { Users, Building2, TrendingUp,Target, Lightbulb } from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
/** 角色视角配置。 */
|
||||
|
||||
@@ -7,7 +7,7 @@ import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { getReport, type MonthlyReport } from "@/lib/reports";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { HealthRadar } from "@/components/health/HealthRadar";
|
||||
import { HealthRadar, DIMENSIONS_4 } from "@/components/health/HealthRadar";
|
||||
import { HealthGauge } from "@/components/health/HealthGauge";
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1";
|
||||
@@ -207,6 +207,7 @@ export default function ReportParsePage({ params }: { params: Promise<{ id: stri
|
||||
ai_commercial: healthScores.ai_commercial_score,
|
||||
ai_cost: healthScores.ai_cost_score,
|
||||
}}
|
||||
dimensions={DIMENSIONS_4}
|
||||
size={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { FileText, Plus, Trash2, Send } from "lucide-react";
|
||||
import {Plus, Trash2, Send } from "lucide-react";
|
||||
import { listReports, submitReport, deleteReport, STATUS_LABELS, STATUS_COLORS, type MonthlyReport } from "@/lib/reports";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
@@ -32,6 +32,7 @@ export default function ReportsPage() {
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadReports();
|
||||
}, [loadReports]);
|
||||
|
||||
|
||||
@@ -4,9 +4,22 @@ import { useState } from "react";
|
||||
import { ReportTemplateSelector, ReportPreview } from "@/components/report/ReportPreview";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
|
||||
/** 报告数据。 */
|
||||
interface ReportData {
|
||||
executive_summary?: string;
|
||||
company_name?: string;
|
||||
financial_performance?: string;
|
||||
operational_highlights?: string;
|
||||
risk_assessment?: string;
|
||||
recommendations?: string[];
|
||||
next_quarter_focus?: string;
|
||||
year_in_review?: string;
|
||||
key_achievements?: string[];
|
||||
}
|
||||
|
||||
/** 投后报告模板选择页。 */
|
||||
export default function ReportTemplatesPage() {
|
||||
const [report, setReport] = useState<any>(null);
|
||||
const [report, setReport] = useState<ReportData | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
async function handleGenerate(type: string, data: string) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ArrowLeft, Download, Loader2 } from "lucide-react";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { HealthGauge } from "@/components/health/HealthGauge";
|
||||
import { HealthRadar } from "@/components/health/HealthRadar";
|
||||
import { HealthRadar, DIMENSIONS_4 } from "@/components/health/HealthRadar";
|
||||
import { RiskTimeline } from "@/components/risk/RiskTimeline";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
|
||||
@@ -168,6 +168,7 @@ export default function ReportViewPage({ params }: { params: Promise<{ id: strin
|
||||
ai_commercial: report.latest_score.ai_commercial_score,
|
||||
ai_cost: report.latest_score.ai_cost_score,
|
||||
}}
|
||||
dimensions={DIMENSIONS_4}
|
||||
size={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function RisksPage() {
|
||||
}, [page, statusFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadRisks();
|
||||
}, [loadRisks]);
|
||||
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Target } from "lucide-react";
|
||||
import { listDecisionSentinels } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { PageContainer, Badge} from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 决策前哨项。 */
|
||||
interface DecisionSentinel {
|
||||
decision_type: string;
|
||||
title: string;
|
||||
status: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function SentinelsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<DecisionSentinel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listDecisionSentinels()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as DecisionSentinel[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -30,15 +37,15 @@ export default function SentinelsPage() {
|
||||
<div key={i} className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="blue">{item.decision_type as string}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title as string}</h3>
|
||||
<Badge color="blue">{item.decision_type}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
</div>
|
||||
<Badge color={item.status === "acted" ? "green" : item.status === "analyzed" ? "amber" : "gray"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="mt-2 text-sm text-gray-600">{item.description as string}</p>
|
||||
<p className="mt-2 text-sm text-gray-600">{item.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Network, Plus } from "lucide-react";
|
||||
import { listSynergies, authorizeSynergy } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 协同机会项。 */
|
||||
interface Synergy {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
status: string;
|
||||
authorized?: boolean;
|
||||
description?: string;
|
||||
match_reason?: string;
|
||||
}
|
||||
|
||||
const SYNERGY_TYPE_LABELS: Record<string, string> = {
|
||||
customer: "客户协同",
|
||||
talent: "人才协同",
|
||||
@@ -17,12 +27,12 @@ const SYNERGY_TYPE_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function SynergiesPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<Synergy[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const load = () => {
|
||||
listSynergies()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as Synergy[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
@@ -51,16 +61,16 @@ export default function SynergiesPage() {
|
||||
<div key={i} className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="blue">{SYNERGY_TYPE_LABELS[item.type as string] ?? item.type}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title as string}</h3>
|
||||
<Badge color="blue">{SYNERGY_TYPE_LABELS[item.type] ?? item.type}</Badge>
|
||||
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color={item.status === "completed" ? "green" : item.status === "authorized" ? "blue" : "gray"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
{item.authorized === false && (
|
||||
<button
|
||||
onClick={() => handleAuthorize(item.id as string)}
|
||||
onClick={() => handleAuthorize(item.id)}
|
||||
className="rounded-md bg-gray-900 px-2 py-1 text-xs text-white hover:bg-gray-700"
|
||||
>
|
||||
授权
|
||||
@@ -68,8 +78,8 @@ export default function SynergiesPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description as string}</p>}
|
||||
{item.match_reason && <p className="mt-1 text-xs text-gray-400">匹配理由:{item.match_reason as string}</p>}
|
||||
{item.description && <p className="mt-2 text-sm text-gray-600">{item.description}</p>}
|
||||
{item.match_reason && <p className="mt-1 text-xs text-gray-400">匹配理由:{item.match_reason}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Users } from "lucide-react";
|
||||
import { listTalents } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 人才项。 */
|
||||
interface Talent {
|
||||
name: string;
|
||||
current_role: string;
|
||||
performance_rating?: string;
|
||||
potential_rating?: string;
|
||||
nine_box?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function TalentsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<Talent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listTalents()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as Talent[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -40,12 +49,12 @@ export default function TalentsPage() {
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-2 text-gray-900">{item.name as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.current_role as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.performance_rating as string ?? "-"}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.potential_rating as string ?? "-"}</td>
|
||||
<td className="px-4 py-2"><Badge color="blue">{item.nine_box as string ?? "-"}</Badge></td>
|
||||
<td className="px-4 py-2"><Badge color={item.status === "active" ? "green" : "gray"}>{item.status as string}</Badge></td>
|
||||
<td className="px-4 py-2 text-gray-900">{item.name}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.current_role}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.performance_rating ?? "-"}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{item.potential_rating ?? "-"}</td>
|
||||
<td className="px-4 py-2"><Badge color="blue">{item.nine_box ?? "-"}</Badge></td>
|
||||
<td className="px-4 py-2"><Badge color={item.status === "active" ? "green" : "gray"}>{item.status}</Badge></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { BarChart3, Plus } from "lucide-react";
|
||||
import {Plus } from "lucide-react";
|
||||
import { listTasks, updateTask } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge } from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/** 任务项。 */
|
||||
interface TaskItem {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
todo: "待办",
|
||||
in_progress: "进行中",
|
||||
@@ -23,12 +32,12 @@ const PRIORITY_COLORS: Record<string, "red" | "amber" | "gray" | "blue"> = {
|
||||
};
|
||||
|
||||
export default function TasksPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<TaskItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const load = () => {
|
||||
listTasks()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as TaskItem[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
@@ -70,15 +79,15 @@ export default function TasksPage() {
|
||||
{items.filter((i) => i.status === col).map((item, i) => (
|
||||
<div key={i} className="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<span className="text-sm font-medium text-gray-900">{item.title as string}</span>
|
||||
<Badge color={PRIORITY_COLORS[item.priority as string] ?? "gray"}>
|
||||
{item.priority as string}
|
||||
<span className="text-sm font-medium text-gray-900">{item.title}</span>
|
||||
<Badge color={PRIORITY_COLORS[item.priority] ?? "gray"}>
|
||||
{item.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.description && <p className="mt-1 text-xs text-gray-500">{item.description as string}</p>}
|
||||
{item.description && <p className="mt-1 text-xs text-gray-500">{item.description}</p>}
|
||||
<select
|
||||
value={item.status as string}
|
||||
onChange={(e) => handleStatusChange(item.id as string, e.target.value)}
|
||||
value={item.status}
|
||||
onChange={(e) => handleStatusChange(item.id, e.target.value)}
|
||||
className="mt-2 w-full rounded border border-gray-200 px-2 py-1 text-xs"
|
||||
>
|
||||
{Object.entries(STATUS_LABELS).map(([k, v]) => (
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Lightbulb } from "lucide-react";
|
||||
import { listWeakSignals } from "@/lib/api-v2";
|
||||
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
|
||||
import { PageContainer, Badge} from "@/components/shared/PageContainer";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 弱信号项。 */
|
||||
interface WeakSignal {
|
||||
signal_type: string;
|
||||
content: string;
|
||||
confidence: number;
|
||||
risk_probability?: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const SIGNAL_TYPE_LABELS: Record<string, string> = {
|
||||
technical: "技术",
|
||||
sentiment: "情绪",
|
||||
@@ -15,12 +23,12 @@ const SIGNAL_TYPE_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function WeakSignalsPage() {
|
||||
const [items, setItems] = useState<Record<string, any>[]>([]);
|
||||
const [items, setItems] = useState<WeakSignal[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listWeakSignals()
|
||||
.then((resp) => setItems((resp.data as Record<string, any>[]) ?? []))
|
||||
.then((resp) => setItems((resp.data as WeakSignal[]) ?? []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
@@ -47,20 +55,20 @@ export default function WeakSignalsPage() {
|
||||
{items.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color="blue">{SIGNAL_TYPE_LABELS[item.signal_type as string] ?? item.signal_type}</Badge>
|
||||
<Badge color="blue">{SIGNAL_TYPE_LABELS[item.signal_type] ?? item.signal_type}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-900 max-w-xs truncate">{item.content as string}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{((item.confidence as number) * 100).toFixed(0)}%</td>
|
||||
<td className="px-4 py-2 text-gray-900 max-w-xs truncate">{item.content}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{(item.confidence * 100).toFixed(0)}%</td>
|
||||
<td className="px-4 py-2">
|
||||
{item.risk_probability ? (
|
||||
<Badge color={(item.risk_probability as number) > 0.6 ? "red" : (item.risk_probability as number) > 0.3 ? "amber" : "green"}>
|
||||
{((item.risk_probability as number) * 100).toFixed(0)}%
|
||||
<Badge color={item.risk_probability > 0.6 ? "red" : item.risk_probability > 0.3 ? "amber" : "green"}>
|
||||
{(item.risk_probability * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
) : "-"}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge color={item.status === "alerted" ? "red" : item.status === "correlated" ? "amber" : "gray"}>
|
||||
{item.status as string}
|
||||
{item.status}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user