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
18 changes: 18 additions & 0 deletions agents/experience_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,14 @@ def _score_entry(entry: dict, query_tokens: list[str]) -> int:

Each token contributes 1, regardless of repetition — avoids a single noisy
field dominating the score. Problem/model_type matches are weighted 2x.

User annotations add a small boost on top of keyword score so highly-rated
or starred entries surface first when scores are close:
- rating (0-5): +rating
- starred (bool): +3
The boost is additive rather than multiplicative so a 5-star entry with
zero keyword hits still can't outrank a direct-match entry — user ratings
guide, not override, relevance.
"""
if not query_tokens:
return 0
Expand All @@ -354,6 +362,16 @@ def _score_entry(entry: dict, query_tokens: list[str]) -> int:
score += 2
elif tok in body:
score += 1

# User-annotation boost
try:
rating = int(entry.get("rating") or 0)
if 0 <= rating <= 5:
score += rating
except (TypeError, ValueError):
pass
if entry.get("starred"):
score += 3
return score


Expand Down
50 changes: 49 additions & 1 deletion ui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

try:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
import uvicorn
except ImportError:
Expand Down Expand Up @@ -1399,6 +1399,54 @@ async def get_experience(phase: str = "", limit: int = 20):
}


@app.post("/api/experience/rate")
async def rate_experience(request: Request):
"""Update an experience entry's user rating / star flag.

Body: {"id": "<entry_id>", "rating": 0-5, "starred": bool}
Both rating and starred are optional — only provided fields are updated.
High-rated / starred entries get a ranking boost in get_relevant_experience.
"""
if not EXPERIENCE_LOG.exists():
return JSONResponse({"ok": False, "error": "experience log missing"}, status_code=404)

try:
body = await request.json()
except Exception:
return JSONResponse({"ok": False, "error": "invalid JSON body"}, status_code=400)

entry_id = str(body.get("id") or "").strip()
if not entry_id:
return JSONResponse({"ok": False, "error": "missing id"}, status_code=400)

try:
data = json.loads(EXPERIENCE_LOG.read_text(encoding="utf-8"))
except Exception as exc:
return JSONResponse({"ok": False, "error": f"corrupted log: {exc}"}, status_code=500)

entries = data.get("entries", [])
target = next((e for e in entries if str(e.get("id")) == entry_id), None)
if target is None:
return JSONResponse({"ok": False, "error": f"no entry with id={entry_id}"}, status_code=404)

if "rating" in body:
try:
r = int(body["rating"])
except (TypeError, ValueError):
return JSONResponse({"ok": False, "error": "rating must be int 0-5"}, status_code=400)
if not 0 <= r <= 5:
return JSONResponse({"ok": False, "error": "rating out of range"}, status_code=400)
target["rating"] = r
if "starred" in body:
target["starred"] = bool(body["starred"])

EXPERIENCE_LOG.write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8",
)
return {"ok": True, "id": entry_id,
"rating": target.get("rating"), "starred": target.get("starred", False)}


# ─────────────────────────────────────────────────────── entry ──

if __name__ == "__main__":
Expand Down
58 changes: 58 additions & 0 deletions ui/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,24 @@
margin-top:12px;font-size:.78rem;color:var(--terracotta);cursor:pointer;
background:none;border:none;padding:0;text-decoration:underline;
}
.exp-rate-row{
display:flex;align-items:center;gap:8px;margin-top:10px;padding-top:10px;
border-top:1px dashed var(--border);
}
.exp-star-btn{
background:none;border:none;cursor:pointer;font-size:1.1rem;color:#d1d5db;
padding:0 2px;line-height:1;transition:color .15s,transform .1s;
}
.exp-star-btn:hover{transform:scale(1.2)}
.exp-star-btn.on{color:#f59e0b}
.exp-fav-btn{
background:none;border:1px solid var(--border);border-radius:12px;
padding:2px 10px;font-size:.72rem;cursor:pointer;color:var(--ink-light);
margin-left:auto;transition:all .15s;
}
.exp-fav-btn:hover{border-color:var(--terracotta);color:var(--terracotta)}
.exp-fav-btn.on{background:#fef3c7;border-color:#f59e0b;color:#92400e;font-weight:600}
.exp-rate-status{font-size:.72rem;color:var(--ink-light);margin-left:4px}
@media(max-width:480px){
.hero{grid-template-columns:1fr}
.page-tabs .page-tab{padding:5px 10px;font-size:.75rem}
Expand Down Expand Up @@ -2441,6 +2459,24 @@ <h3>${escapeHtml(f.filename)} <span class="badge ${f.status==='pass'?'pass':'fai
}).join('')}</div>
</div>` : '';

const entryId = e.id || '';
const rating = Number.isInteger(e.rating) ? e.rating : 0;
const starred = !!e.starred;
const stars = [1,2,3,4,5].map(n =>
`<button class="exp-star-btn ${n<=rating?'on':''}" title="评 ${n} 星"
onclick="rateExperience('${entryId}', ${n===rating?0:n}, null, this)">★</button>`
).join('');
const rateHtml = entryId ? `
<div class="exp-rate-row">
<span style="font-size:.72rem;color:var(--ink-light)">评分</span>
${stars}
<span class="exp-rate-status" id="rate-status-${idx}"></span>
<button class="exp-fav-btn ${starred?'on':''}"
onclick="rateExperience('${entryId}', null, ${!starred}, this)">
${starred?'★ 已收藏':'☆ 收藏'}
</button>
</div>` : '';

return `
<div class="exp-card" id="${id}">
<div class="exp-card-header">
Expand All @@ -2451,10 +2487,32 @@ <h3>${escapeHtml(f.filename)} <span class="badge ${f.status==='pass'?'pass':'fai
${tipsHtml}
<div class="exp-fields" style="margin-top:${tips.length?'12px':'0'}">${fieldHtml}</div>
${moreHtml}
${rateHtml}
</div>`;
}).join('');
}

async function rateExperience(id, rating, starred, btnEl) {
if (!id) return;
const body = {id};
if (rating !== null) body.rating = rating;
if (starred !== null) body.starred = starred;
try {
const r = await fetch('/api/experience/rate', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(body),
});
const d = await r.json();
if (!r.ok || !d.ok) throw new Error(d.error || r.statusText);
// Refresh the view so star/fav state reflects the server
const activePhase = document.querySelector('.exp-filter-btn.active')?.dataset.phase || '';
loadExperience(activePhase);
} catch (err) {
console.error('rate failed', err);
if (btnEl) btnEl.title = '保存失败:' + err.message;
}
}

function toggleExpDetail(id, btn) {
const el = document.getElementById(id);
const open = el.style.display === 'block';
Expand Down
Loading