Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app-ai-native/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ src/assets/theme.css
e2e/test-results
e2e/playwright-report
.env
.env.local
.env.*.local
12 changes: 12 additions & 0 deletions packages/app-ai-native/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions packages/app-ai-native/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "风险等级",
Expand Down
124 changes: 124 additions & 0 deletions packages/app-ai-native/src/pages/store/components/health-radar.tsx
Original file line number Diff line number Diff line change
@@ -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<SignalKey, { x: number; y: number; anchor: "start" | "middle" | "end" }> = {
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 (
<svg
width="100%"
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
class="mx-auto block"
style={{ "max-width": `${WIDTH}px`, height: "auto" }}
>
{/* Grid polygons */}
<For each={GRID_LEVELS}>
{(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 <path d={d} fill="none" stroke={gridColor()} stroke-width={1} />
}}
</For>

{/* Axis lines */}
<For each={AXES}>
{(a) => {
const end = polar(a.angle, RADIUS)
return <line x1={CX} y1={CY} x2={end.x} y2={end.y} stroke={gridColor()} stroke-width={1} />
}}
</For>

{/* Data polygon */}
<path
d={dataPath()}
fill={`color-mix(in srgb, ${accent()} 15%, transparent)`}
stroke={accent()}
stroke-width={2}
/>

{/* Data dots */}
<For each={dataPoints()}>{(p) => <circle cx={p.x} cy={p.y} r={3} fill={accent()} />}</For>

{/* Labels — placed at fixed edge positions */}
<For each={AXES}>
{(axis) => {
const lp = LABEL_POS[axis.key]
return (
<text
x={lp.x}
y={lp.y}
text-anchor={lp.anchor}
dominant-baseline="middle"
fill={labelColor()}
style={{ "font-size": "11px" }}
>
{language.t(axis.i18nKey)} ({Math.round(value(axis.key))})
</text>
)
}}
</For>
</svg>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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: {
Expand Down Expand Up @@ -420,6 +448,89 @@ export default function ItemDetailContent(props: ItemDetailContentProps) {
<p class="text-[13px] leading-6 text-text-weak">{pickItemDescription(data(), language.locale())}</p>
</Show>

<Show when={hasHealthSignals(data().health) || hasEvaluation(data().evaluation)}>
<div class="flex flex-wrap gap-4">
<Show when={hasHealthSignals(data().health)}>
<div class="flex-1 min-w-[260px] rounded-[var(--native-radius-md)] border border-border-weak-base bg-bg-muted/40 p-4">
<div
class="mb-2 text-xs"
style={{
color: "color-mix(in srgb, var(--native-muted) 70%, var(--native-panel))",
"font-weight": 700,
}}
>
{language.t("store.detail.health.title")}
</div>
<HealthRadar signals={data().health!.signals} accent={meta().accent} />
</div>
</Show>

<Show when={hasEvaluation(data().evaluation) && data().evaluation}>
{(evaluation) => (
<div class="flex-1 min-w-[260px] rounded-[var(--native-radius-md)] border border-border-weak-base bg-bg-muted/40 p-4">
<div class="mb-3 flex items-center justify-between gap-4">
<div
class="text-xs"
style={{
color: "color-mix(in srgb, var(--native-muted) 70%, var(--native-panel))",
"font-weight": 700,
}}
>
{language.t("store.detail.eval.title")}
</div>
<Show when={evaluation().final_score > 0}>
<span class="text-lg font-bold" style={{ color: meta().accent }}>
{Math.round(evaluation().final_score)}
</span>
</Show>
</div>
<div class="space-y-2.5">
<For each={EVAL_DIMS}>
{(dim) => {
const val = evaluation()[dim]
return (
<Show when={val != null}>
<div class="flex items-center gap-3">
<span class="w-28 shrink-0 text-[11px] text-text-weak">
{language.t("store.detail.eval." + dim)}
</span>
<div class="flex flex-1 gap-1">
<For each={[1, 2, 3, 4, 5]}>
{(seg) => (
<div
class="h-2 flex-1 rounded-full"
style={{
"background-color":
seg <= (val as number)
? meta().accent
: "color-mix(in srgb, var(--native-muted) 22%, var(--native-panel))",
}}
/>
)}
</For>
</div>
<span class="w-4 text-right text-[11px] text-text-weak">{val as number}</span>
</div>
</Show>
)
}}
</For>
</div>
<Show when={evaluation().evaluated_at}>
<p class="mt-3 text-[11px] text-text-weak">
{language.t("store.detail.eval.evaluator")}:{" "}
{evaluation().model_id === "__cached__"
? "deepseek-chat"
: evaluation().model_id || "unknown"}{" "}
· {formatDate(evaluation().evaluated_at!, language.locale())}
</p>
</Show>
</div>
)}
</Show>
</div>
</Show>

<Show when={data().content}>
<div>
<Show
Expand Down
19 changes: 19 additions & 0 deletions packages/app-ai-native/src/pages/store/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ export interface CapabilityItem {
artifacts?: CapabilityArtifact[]
assets?: CapabilityItemAsset[]
tags?: ItemTag[]
health?: {
score?: number
signals: { freshness: number; popularity: number; source_trust: number }
freshness_label?: string
last_commit?: string
}
evaluation?: {
coding_relevance?: number
doc_completeness?: number
desc_accuracy?: number
writing_quality?: number
specificity?: number
install_clarity?: number
final_score: number
decision?: string
model_id?: string
rubric_version?: string
evaluated_at?: string
}
}

export type ItemSort = "favoriteCount" | "installCount" | "previewCount" | "experienceScore" | "updatedAt"
Expand Down
Loading