diff --git a/packages/app-ai-native/.gitignore b/packages/app-ai-native/.gitignore index 5d2f820ae..24d467eb6 100644 --- a/packages/app-ai-native/.gitignore +++ b/packages/app-ai-native/.gitignore @@ -2,4 +2,5 @@ src/assets/theme.css e2e/test-results e2e/playwright-report .env +.env.local .env.*.local diff --git a/packages/app-ai-native/src/i18n/en.ts b/packages/app-ai-native/src/i18n/en.ts index 5212b39de..bf05c2bee 100644 --- a/packages/app-ai-native/src/i18n/en.ts +++ b/packages/app-ai-native/src/i18n/en.ts @@ -576,6 +576,18 @@ export const dict = { "store.detail.share.qrcode": "Scan QR Code", "store.detail.share.copyLink": "Copy Link", "store.detail.share.copied": "Copied", + "store.detail.health.title": "Health", + "store.detail.health.popularity": "Popularity", + "store.detail.health.freshness": "Freshness", + "store.detail.health.source_trust": "Source Trust", + "store.detail.eval.title": "Evaluation", + "store.detail.eval.coding_relevance": "Coding Relevance", + "store.detail.eval.doc_completeness": "Doc Completeness", + "store.detail.eval.desc_accuracy": "Description Accuracy", + "store.detail.eval.writing_quality": "Writing Quality", + "store.detail.eval.specificity": "Specificity", + "store.detail.eval.install_clarity": "Install Clarity", + "store.detail.eval.evaluator": "Evaluator", "store.scanResults": "Scan Results", "store.scanResults.verdict": "Verdict", "store.scanResults.risk": "Risk Level", diff --git a/packages/app-ai-native/src/i18n/zh.ts b/packages/app-ai-native/src/i18n/zh.ts index 89d1e3976..d7347436a 100644 --- a/packages/app-ai-native/src/i18n/zh.ts +++ b/packages/app-ai-native/src/i18n/zh.ts @@ -1481,6 +1481,18 @@ export const dict = { "store.detail.share.qrcode": "扫码分享", "store.detail.share.copyLink": "复制链接", "store.detail.share.copied": "已复制", + "store.detail.health.title": "健康度", + "store.detail.health.popularity": "流行度", + "store.detail.health.freshness": "活跃度", + "store.detail.health.source_trust": "来源可信度", + "store.detail.eval.title": "评估详情", + "store.detail.eval.coding_relevance": "编程相关性", + "store.detail.eval.doc_completeness": "文档完整度", + "store.detail.eval.desc_accuracy": "描述准确性", + "store.detail.eval.writing_quality": "写作质量", + "store.detail.eval.specificity": "专业度", + "store.detail.eval.install_clarity": "安装清晰度", + "store.detail.eval.evaluator": "评估模型", "store.scanResults": "扫描结果", "store.scanResults.verdict": "结论", "store.scanResults.risk": "风险等级", diff --git a/packages/app-ai-native/src/pages/store/components/health-radar.tsx b/packages/app-ai-native/src/pages/store/components/health-radar.tsx new file mode 100644 index 000000000..dba4c46ae --- /dev/null +++ b/packages/app-ai-native/src/pages/store/components/health-radar.tsx @@ -0,0 +1,124 @@ +import { For } from "solid-js" +import { useTheme } from "@opencode-ai/ui/theme" +import { useLanguage } from "@/context/language" + +interface HealthRadarProps { + signals: { + freshness: number + popularity: number + source_trust: number + } + /** Accent color for the data polygon; falls back to the native primary token. */ + accent?: string +} + +type SignalKey = "popularity" | "freshness" | "source_trust" + +const AXES: { key: SignalKey; i18nKey: string; angle: number }[] = [ + { key: "popularity", i18nKey: "store.detail.health.popularity", angle: -90 }, + { key: "freshness", i18nKey: "store.detail.health.freshness", angle: 30 }, + { key: "source_trust", i18nKey: "store.detail.health.source_trust", angle: 150 }, +] + +// Layout: generous padding so labels never clip +const PAD_X = 80 // horizontal padding for left/right labels +const PAD_Y = 30 // vertical padding for top/bottom labels +const RADIUS = 65 +const CX = PAD_X + RADIUS +const CY = PAD_Y + RADIUS +const WIDTH = 2 * (PAD_X + RADIUS) +const HEIGHT = 2 * (PAD_Y + RADIUS) +const GRID_LEVELS = [0.25, 0.5, 0.75, 1] + +function polar(angle: number, r: number) { + const rad = (angle * Math.PI) / 180 + return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) } +} + +// Label positions: place them outside the chart near each vertex +const LABEL_POS: Record = { + popularity: { x: CX, y: PAD_Y - 14, anchor: "middle" }, // top center + freshness: { x: WIDTH - 16, y: CY + RADIUS * 0.6, anchor: "end" }, // bottom right + source_trust: { x: 16, y: CY + RADIUS * 0.6, anchor: "start" }, // bottom left +} + +export default function HealthRadar(props: HealthRadarProps) { + const language = useLanguage() + const theme = useTheme() + + const isDark = () => theme.mode() === "dark" + const accent = () => props.accent ?? "var(--native-primary)" + // Grid/axis lines derive from the muted/border tokens; nudge stronger in dark mode. + const gridColor = () => + isDark() + ? "color-mix(in srgb, var(--native-muted) 45%, var(--native-panel))" + : "color-mix(in srgb, var(--native-muted) 22%, var(--native-panel))" + const labelColor = () => "var(--native-muted)" + + const value = (key: SignalKey) => props.signals[key] || 0 + + const dataPoints = () => AXES.map((a) => polar(a.angle, RADIUS * (value(a.key) / 100))) + const dataPath = () => + dataPoints() + .map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`) + .join(" ") + "Z" + + return ( + + {/* Grid polygons */} + + {(level) => { + const d = + AXES.map((a, i) => { + const p = polar(a.angle, RADIUS * level) + return `${i === 0 ? "M" : "L"}${p.x},${p.y}` + }).join(" ") + "Z" + return + }} + + + {/* Axis lines */} + + {(a) => { + const end = polar(a.angle, RADIUS) + return + }} + + + {/* Data polygon */} + + + {/* Data dots */} + {(p) => } + + {/* Labels — placed at fixed edge positions */} + + {(axis) => { + const lp = LABEL_POS[axis.key] + return ( + + {language.t(axis.i18nKey)} ({Math.round(value(axis.key))}) + + ) + }} + + + ) +} diff --git a/packages/app-ai-native/src/pages/store/components/item-detail-content.tsx b/packages/app-ai-native/src/pages/store/components/item-detail-content.tsx index 346f88ec3..b433bcba0 100644 --- a/packages/app-ai-native/src/pages/store/components/item-detail-content.tsx +++ b/packages/app-ai-native/src/pages/store/components/item-detail-content.tsx @@ -16,6 +16,7 @@ import { itemApi, userApi, type CapabilityItem } from "../lib/api" import { useLanguage } from "@/context/language" import { pickItemDescription } from "../lib/item-description" import SecurityTag from "./security-tag" +import HealthRadar from "./health-radar" import { DistributeDialog } from "./distribute-dialog" import "@/styles/vscode-markdown.css" @@ -30,6 +31,33 @@ const TYPE_META: Record< plugin: { accent: "#EC4899", bg: "color-mix(in srgb, #EC4899 12%, var(--native-panel))", label: "store.sidebar.nav.plugins", icon: "configuration" }, } +const EVAL_DIMS = [ + "coding_relevance", + "doc_completeness", + "desc_accuracy", + "writing_quality", + "specificity", + "install_clarity", +] as const + +function hasHealthSignals(health?: CapabilityItem["health"]) { + const s = health?.signals + return !!s && (s.freshness != null || s.popularity != null || s.source_trust != null) +} + +function hasEvaluation(e?: CapabilityItem["evaluation"]) { + if (!e) return false + return ( + (e.final_score != null && e.final_score > 0) || + e.coding_relevance != null || + e.doc_completeness != null || + e.desc_accuracy != null || + e.writing_quality != null || + e.specificity != null || + e.install_clarity != null + ) +} + const THEMES = { light: "light-plus", dark: "dark-plus" } as const const TAG_COLOR_BY_CLASS = { system: { @@ -420,6 +448,89 @@ export default function ItemDetailContent(props: ItemDetailContentProps) {

{pickItemDescription(data(), language.locale())}

+ +
+ +
+
+ {language.t("store.detail.health.title")} +
+ +
+
+ + + {(evaluation) => ( +
+
+
+ {language.t("store.detail.eval.title")} +
+ 0}> + + {Math.round(evaluation().final_score)} + + +
+
+ + {(dim) => { + const val = evaluation()[dim] + return ( + +
+ + {language.t("store.detail.eval." + dim)} + +
+ + {(seg) => ( +
+ )} + +
+ {val as number} +
+ + ) + }} + +
+ +

+ {language.t("store.detail.eval.evaluator")}:{" "} + {evaluation().model_id === "__cached__" + ? "deepseek-chat" + : evaluation().model_id || "unknown"}{" "} + · {formatDate(evaluation().evaluated_at!, language.locale())} +

+
+
+ )} + +
+
+