import { BUILD_ID } from "../build"; import type { IssueAction } from "../issue-actions-hook"; export interface LayoutProps { title: string; issueTitle: string; repoPath: string; issueNumber: number; upstreamUrl?: string; state: string; totalComments: number; descriptionHtml: string; descriptionCollapsed: boolean; bodyClass?: string; writesEnabled?: boolean; operatorLogin?: string; upstreamWebUrl?: string | null; translateEnabled?: boolean; ttsEnabled?: boolean; labels?: { id: number; name: string; color: string }[]; canEditLabels?: boolean; aiStatus?: string; projectDispatchOff?: boolean; viewerLogin?: string; viewerIsAdmin?: boolean; customActions?: IssueAction[]; extraStatusBadges?: Record; modelSelect?: { current: string; options: { id: string; label: string }[] } | null; runtimeSelect?: { current: string } | null; authorLineHtml?: string; } export const THEME_CSS = ` :root{ --bg:#ffffff;--bg-muted:#f5f5f5;--bg-elev:#ffffff;--border:#e1e4e8;--text:#24292e;--text-muted:#6a737d; --accent:#2185d0;--green:#2da44e;--header-bg:#2c3338;--header-text:#e6edf3; --human:#0969da;--bot:#2da44e;--system:#8957e5;--code-bg:#f6f8fa; } @media (prefers-color-scheme:dark){:root{ --bg:#1b1b1b;--bg-muted:#232323;--bg-elev:#262626;--border:#373737;--text:#e6e6e6;--text-muted:#9a9a9a; --accent:#4493f8;--green:#3fb950;--header-bg:#161616;--header-text:#e6edf3; --human:#4493f8;--bot:#3fb950;--system:#bc8cff;--code-bg:#0d1117; }} *{box-sizing:border-box} mark.hl{background:#ffe082;color:#b71c1c;font-weight:700;border-radius:2px;padding:0 2px} @media (prefers-color-scheme:dark){mark.hl{background:#5d4037;color:#ff8a80}} .fsnip{color:var(--text-muted);font-size:13px;margin-top:.2rem;line-height:1.4;overflow-wrap:anywhere} html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft YaHei",sans-serif} a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline} header.topbar{background:var(--header-bg);color:var(--header-text);padding:.55rem 1rem;display:flex;align-items:center;gap:.6rem;font-size:13px;flex-wrap:wrap} header.topbar .repo{opacity:.85} header.topbar .num{opacity:.7} .state-badge{font-size:12px;padding:.1rem .5rem;border-radius:10px;font-weight:600} .state-open{background:#1a7f37;color:#fff}.state-closed{background:#8250df;color:#fff} .meta-bar{display:flex;flex-direction:column;gap:.3rem;padding:.7rem max(1rem,calc((100% - 900px)/2));border-bottom:1px solid var(--border);background:var(--bg-muted)} .meta-bar h1{font-size:18px;margin:0;font-weight:600;overflow-wrap:anywhere;word-break:break-word} .meta-status{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;font-size:13px;color:var(--text-muted)} .action-group{display:flex;gap:.3rem;align-items:center;padding-left:.5rem;margin-left:.2rem;border-left:1px solid var(--border)} .label-group{display:flex;gap:.3rem;align-items:center;padding-left:.5rem;margin-left:.2rem;border-left:1px solid var(--border);flex-wrap:wrap} .count{color:var(--text-muted);font-size:12px;white-space:nowrap} #list{padding:.5rem .6rem 4rem;max-width:900px;margin:0 auto} .sentinel{height:1px} .loader,#loadOlderWrap{display:flex;justify-content:center;padding:1rem;color:var(--text-muted);font-size:13px} #loadOlder{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:.5rem 1.2rem;color:var(--text);cursor:pointer;font-size:13px} #loadOlder:hover{border-color:var(--accent)} .item{padding:.7rem .2rem;position:relative} .card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;min-width:0} .card-h{display:flex;align-items:center;gap:.5rem;padding:.4rem .7rem;background:var(--bg-muted);font-size:13px;flex-wrap:wrap} .card-h .who{font-weight:600;color:var(--text)} .card-h .who-login{font-weight:400;color:var(--text-muted);font-size:.85em;margin-left:.25rem} .card-h .cmodel{background:var(--bg-elev);border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)} .tag{font-size:11px;font-weight:600;padding:.05rem .4rem;border-radius:4px;line-height:1.5} .tag-human{background:color-mix(in srgb,var(--human) 18%,transparent);color:var(--human)} .tag-bot{background:color-mix(in srgb,var(--bot) 18%,transparent);color:var(--bot)} .tag-system{background:color-mix(in srgb,var(--system) 18%,transparent);color:var(--system)} .when{color:var(--text-muted);font-size:12px} .clink{background:none;border:none;cursor:pointer;font-size:13px;color:var(--text-muted);padding:0 .2rem;opacity:.6;line-height:1.5} .clink:hover{opacity:1;color:var(--accent)} .clink.done{color:var(--green);opacity:1} .card-actions{margin-left:auto;display:inline-flex;gap:.3rem;align-items:center} .ttsbtn{background:none;border:none;cursor:pointer;font-size:13px;color:var(--text-muted);padding:0 .2rem;opacity:.6;line-height:1.5} .ttsbtn:hover{opacity:1;color:var(--accent)} .ttsbtn.playing{color:var(--green);opacity:1} .ttsstop{background:none;border:none;cursor:pointer;font-size:13px;color:var(--text-muted);padding:0 .2rem;line-height:1.5} .ttsstop:hover{color:var(--accent)} .cbtn,.tbtn{background:none;border:none;cursor:pointer;font-size:13px;color:var(--text-muted);padding:0 .2rem;opacity:.6;line-height:1.5} .cbtn:hover,.tbtn:hover{opacity:1;color:var(--accent)} .wlbtn{background:none;border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:11px;color:var(--text-muted);padding:.05rem .35rem;margin-right:.3rem;line-height:1.5} .wlbtn:hover{color:var(--accent);border-color:var(--accent)} .clink:hover{opacity:1;color:var(--accent)} .clink.done{color:var(--green);opacity:1} .rx{display:inline-flex;gap:.3rem;align-items:center;margin-left:.2rem;flex-wrap:wrap} .rxc{display:inline-flex;align-items:center;gap:.15rem;font-size:12px;background:var(--bg-muted);border:1px solid var(--border);border-radius:10px;padding:0 .4rem;line-height:1.6} .rxn{font-size:11px;color:var(--text-muted)} .card-b{padding:.6rem .8rem;overflow-wrap:anywhere;word-break:break-word} .card-b pre{background:var(--code-bg);padding:.7rem;border-radius:6px;overflow:auto;font-size:12.5px} .card-b code{background:var(--code-bg);padding:.1em .35em;border-radius:4px;font-size:12.5px} .card-b pre code{background:none;padding:0} .card-b img,.desc img{max-width:100%;height:auto} .card-b table,.desc table{border-collapse:collapse;margin:.6rem 0;font-size:13px} .card-b th,.card-b td,.desc th,.desc td{border:1px solid var(--border);padding:.35rem .55rem;text-align:left;vertical-align:top} .card-b th,.desc th{background:var(--bg-muted);font-weight:600} .item-system .card{border-style:dashed;opacity:.86} .item-system .card-b{font-size:13px;color:var(--text-muted)} .hidden{display:none!important} .new-marker{background:color-mix(in srgb,var(--green) 12%,transparent);transition:background 4s ease} .composer{display:flex;flex-direction:column;gap:.5rem;padding:.7rem 1rem;max-width:900px;margin:0 auto .9rem} .composer textarea{width:100%;resize:vertical;min-height:6em;max-height:18em;background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.6rem .7rem;font:inherit;font-size:15px;line-height:1.5} .composer .submit-col{display:flex;flex-direction:row;gap:.5rem;justify-content:flex-end;align-items:center} .composer .upload-btn{background:var(--bg-muted);color:var(--text-muted);border:1px solid var(--border);border-radius:8px;padding:.4rem .6rem;font-size:18px;line-height:1;cursor:pointer;position:relative;overflow:hidden} .composer .upload-btn input{position:absolute;inset:0;opacity:0;cursor:pointer} .composer textarea:focus{outline:none;border-color:var(--accent)} .composer button{flex-shrink:0;background:var(--green);color:#fff;border:none;border-radius:8px;padding:.55rem 1.1rem;font:600 13px system-ui,sans-serif;cursor:pointer} .composer button:disabled{opacity:.5;cursor:default} .composer .btn-close{background:var(--bg-muted);color:var(--text);border:1px solid var(--border);margin-right:auto} .composer .btn-close:hover{border-color:var(--accent)} .composer .btn-close.armed{background:#d23f31;color:#fff;border-color:#d23f31} .composer-ro{max-width:900px;margin:0 auto .9rem;padding:.7rem 1rem;color:var(--text-muted);font-size:13px;text-align:center;border:1px dashed var(--border);border-radius:8px} .tabs{display:flex;gap:.4rem;padding:.6rem 1rem;border-bottom:1px solid var(--border);max-width:900px;margin:0 auto;align-items:center} .tab{padding:.4rem 1rem;border-radius:6px;font-size:14px;color:var(--text-muted)} .tab.active{background:var(--bg-muted);color:var(--text);font-weight:600} .tab-spacer{flex:1} .user-area{display:flex;align-items:center;gap:.3rem} .logout-form{display:inline} .logout-btn{background:none;border:1px solid var(--border);border-radius:6px;padding:.3rem .8rem;font-size:13px;color:var(--text-muted);cursor:pointer;font:inherit} .logout-btn:hover{border-color:var(--accent);color:var(--accent)} .desc-wrap{max-width:900px;margin:0 auto;padding:.5rem 1rem} .desc-actions{margin-bottom:.4rem;display:flex;gap:.3rem;justify-content:flex-end} .desc{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:.6rem .8rem;overflow:hidden;overflow-wrap:anywhere;word-break:break-word} .desc.collapsed{max-height:8.5em;position:relative} .desc.collapsed::after{content:"";position:absolute;inset:auto 0 0 0;height:2.6em;background:linear-gradient(transparent,var(--bg-elev))} .desc-toggle{margin-top:.4rem;background:none;border:none;color:var(--accent);font-size:13px;cursor:pointer;padding:.2rem 0} .upstream-link{font-size:12px;color:var(--accent);opacity:.85} .upstream-link:hover{opacity:1;text-decoration:underline} .issue-label{display:inline-flex;align-items:center;font-size:12px;font-weight:500;padding:.05rem .5rem;border:1px solid;border-radius:99px;line-height:1.6;background:color-mix(in srgb,currentColor 8%,transparent)} .label-edit-btn{background:none;border:1px solid var(--border);border-radius:6px;padding:.05rem .35rem;cursor:pointer;font-size:13px;line-height:1.5;color:var(--text-muted)} .label-edit-btn:hover{border-color:var(--accent);color:var(--accent)} .model-select-wrap{display:inline-flex;align-items:center;gap:.25rem} .model-select{background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:.15rem .3rem;font:12px system-ui,sans-serif;max-width:180px} .model-save-btn{padding:.15rem .45rem;font-size:13px} #labelDlg{border:1px solid var(--border);border-radius:10px;background:var(--bg-elev);color:var(--text);padding:1.1rem;max-width:380px;width:90vw} #labelDlg::backdrop{background:rgba(0,0,0,.5)} #labelDlg h3{margin:0 0 .6rem;font-size:14px} .lp-list{max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:.15rem} .lp-item{display:flex;align-items:center;gap:.4rem;padding:.3rem .4rem;border-radius:6px;cursor:pointer;font-size:13px} .lp-item:hover{background:var(--bg-muted)} .lp-item input{margin:0} .lp-dot{width:10px;height:10px;border-radius:50%;border:1px solid var(--border);flex-shrink:0} .lp-name{flex:1;overflow-wrap:anywhere} .lp-scope{font-size:10px;color:var(--text-muted)} .lp-empty{color:var(--text-muted);font-size:12px;padding:.6rem 0;text-align:center} .ai-badge{font-size:12px;padding:.1rem .5rem;border-radius:10px;font-weight:600} .ai-processing{background:#0969da;color:#fff;animation:ai-pulse 2s ease-in-out infinite} .ai-halted{background:#bf8700;color:#fff} .ai-dispatch-off{background:#6f7781;color:#fff} .ai-completed{background:#1a7f37;color:#fff} .ai-failed{background:#cf222e;color:#fff} .ai-idle{background:#eaeef2;color:#57606a} @keyframes ai-pulse{0%,100%{opacity:1}50%{opacity:.6}} .action-btn{font-size:12px;padding:.15rem .6rem;border-radius:6px;border:1px solid var(--border);background:var(--bg-elev);color:#cf222e;cursor:pointer;font-weight:600} .action-btn:hover{background:#cf222e;color:#fff} .action-btn.resume-btn{color:#1a7f37} .action-btn.resume-btn:hover{background:#1a7f37;color:#fff} .action-btn.dispatch-btn{color:#6f7781} .action-btn.dispatch-btn:hover{background:#6f7781;color:#fff} .action-btn.custom-btn{color:var(--accent)} .action-btn.custom-btn:hover{background:var(--accent);color:#fff} .author-line{color:var(--fg-muted);font-size:.9em;margin:2px 0 6px} .author-line .wl-form{display:inline;margin-left:8px} .author-line .kind-tag{font-size:.75em;border:1px solid var(--border);border-radius:8px;padding:0 6px;margin-left:4px;color:var(--fg-muted)} `; export function renderLayout(props: LayoutProps, inner: string, initialItems: string): string { const stateClass = props.state === "closed" ? "state-closed" : "state-open"; const stateLabel = props.state === "closed" ? "Closed" : "Open"; const isClosed = props.state === "closed"; const toggleAction = isClosed ? "reopen" : "close"; const toggleLabel = isClosed ? "评论并重新打开" : "评论并关闭"; const toggleTitle = isClosed ? "发评论并重新打开工单" : "发评论并关闭工单"; const [repoOwner, repoName] = props.repoPath.split("/"); const repoIssuesHref = `/${encodeURIComponent(repoOwner ?? "")}/${encodeURIComponent(repoName ?? "")}/issues`; const op = props.operatorLogin ?? "operator"; const labelsHtml = (props.labels ?? []).length ? props.labels!.map((l) => `${escapeHtml(l.name)}`).join("") : ""; const labelPickerBtn = props.canEditLabels ? `` : ""; const aiBadgeHtml = (() => { const s = props.aiStatus ?? ""; const map: Record = { processing: { cls: "ai-processing", label: "🔄 AI 处理中" }, queued: { cls: "ai-queued", label: "⏳ 排队中" }, completed: { cls: "ai-completed", label: "✅ AI 已完成" }, failed: { cls: "ai-failed", label: "⚠️ AI 失败" }, halted: { cls: "ai-halted", label: "⏹ 已停止" }, dispatch_off: { cls: "ai-dispatch-off", label: "🔕 不接单中" }, idle: { cls: "ai-idle", label: "💤 空闲" }, ...props.extraStatusBadges, }; const m = map[s] ?? { cls: "ai-idle", label: "💤 空闲" }; return `${m.label}`; })(); const showActions = props.writesEnabled !== false; const haltBtnHtml = showActions ? (props.aiStatus === "halted" || props.aiStatus === "failed") ? `` : props.aiStatus === "processing" ? `` : "" : ""; const dispatchBtnHtml = showActions ? props.projectDispatchOff ? `🔕 项目不接单` : props.aiStatus === "dispatch_off" ? `` : `` : ""; const resetBtnHtml = showActions ? `` : ""; const runtimeSelectHtml = props.runtimeSelect && showActions ? `` : ""; const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions ? `` : ""; return ` ${escapeHtml(props.title)}
🏠 / ${escapeHtml(props.repoPath)} #${props.issueNumber} ${props.upstreamUrl ? `` : ""}

${escapeHtml(props.issueTitle)}

${props.authorLineHtml ?? ""}
${stateLabel} ${aiBadgeHtml} ${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `${haltBtnHtml}${dispatchBtnHtml}${resetBtnHtml}${runtimeSelectHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => { const attrs = [`data-action-href="${escapeAttr(a.href)}"`]; if (a.method && a.method !== "POST") attrs.push(`data-action-method="${escapeAttr(a.method)}"`); if (a.confirm) attrs.push(`data-action-confirm="${escapeAttr(a.confirm)}"`); if (a.reloadOnOk === false) attrs.push(`data-action-reload="false"`); const cls = a.className ? `${escapeAttr(a.className)}` : "action-btn custom-btn"; return ``; }).join("")}` : ""} ${(labelsHtml || labelPickerBtn) ? `${labelsHtml}${labelPickerBtn}` : ""} ${props.upstreamWebUrl ? `🔗 查看上游` : ""}
${props.descriptionHtml.trim() ? `
${actionBarHTML({ copy: true, link: true, translate: true, tts: true, translateEnabled: props.translateEnabled !== false, ttsEnabled: props.ttsEnabled !== false })}
${props.descriptionHtml}
${props.descriptionCollapsed ? `` : ""}
` : ""} ${props.writesEnabled !== false ? `
` : `
📝 只读模式:评论 / 上传未启用(WORK_WRITES_ENABLED=false)
`}
${initialItems}
${props.canEditLabels ? `

标签

` : ""} `; } export function escapeHtml(s: string): string { return (s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export function aiStatusBadge(status: string | undefined): string { const s = status ?? ""; if (!s) return ""; const map: Record = { processing: '⚙️ 处理中', queued: '⏳ 排队中', halted: '⏹️ 已停止', dispatch_off: '🔕 不接单', completed: '✓ 已完成', failed: '✗ 失败', }; return map[s] ?? ""; } export function escapeAttr(s: string): string { return escapeHtml(s); } export function containsCI(haystack: string, needle: string): boolean { if (!needle) return false; return haystack.toLowerCase().includes(needle.toLowerCase()); } export function highlightAll(text: string, needle: string): string { const esc = escapeHtml(text); if (!needle) return esc; const n = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return esc.replace(new RegExp(`(${n})`, "gi"), '$1'); } export function snipHighlight(text: string, needle: string, radius = 50): string { if (!needle) return ""; const flat = text.replace(/\s+/g, " "); const lo = flat.toLowerCase(); const n = needle.toLowerCase(); const idx = lo.indexOf(n); if (idx < 0) return ""; const start = Math.max(0, idx - radius); const end = Math.min(flat.length, idx + n.length + radius); const prefix = start > 0 ? "…" : ""; const suffix = end < flat.length ? "…" : ""; return `${prefix}${escapeHtml(flat.slice(start, idx))}${escapeHtml(flat.slice(idx, idx + n.length))}${escapeHtml(flat.slice(idx + n.length, end))}${suffix}`; } export function tabNavHTML(active: "projects" | "issues" | "sessions" | "me", user?: { login: string; is_admin: number | boolean }): string { const p = active === "projects" ? " active" : ""; const i = active === "issues" ? " active" : ""; const s = active === "sessions" ? " active" : ""; const m = active === "me" ? " active" : ""; const userArea = user ? `👤 ${escapeHtml(user.login)}
` : ""; return ``; } export interface ActionBarOpts { cid?: string; copy?: boolean; link?: boolean; translate?: boolean; tts?: boolean; translateEnabled?: boolean; ttsEnabled?: boolean; } export function actionBarHTML(o: ActionBarOpts): string { const d = o.cid != null ? ` data-cid="${escapeAttr(o.cid)}"` : ""; const cpy = o.copy ? `` : ""; const lnk = o.link ? `` : ""; const tr = (o.translate && o.translateEnabled !== false) ? `` : ""; const tts = (o.tts && o.ttsEnabled !== false) ? `` : ""; return cpy + lnk + tr + tts; }