Spaces:
Sleeping
Sleeping
Simplify chat composer and add optional repo context.
Browse filesRemove the topic selector and separate repo field, put the send button inline with the message input, and persist repo via query param or saved chats for system prompts.
- README.md +1 -0
- app.py +1 -0
- index.html +26 -114
README.md
CHANGED
|
@@ -27,6 +27,7 @@ Frontend query params (browser-side):
|
|
| 27 |
| --- | --- | --- |
|
| 28 |
| `api` | `http://localhost:8000/v1` | OpenAI-compatible base URL for the inference server. |
|
| 29 |
| `model` | `Qwen/Qwen3-0.6B` | Model id sent in the chat completion request. |
|
|
|
|
| 30 |
|
| 31 |
Example:
|
| 32 |
|
|
|
|
| 27 |
| --- | --- | --- |
|
| 28 |
| `api` | `http://localhost:8000/v1` | OpenAI-compatible base URL for the inference server. |
|
| 29 |
| `model` | `Qwen/Qwen3-0.6B` | Model id sent in the chat completion request. |
|
| 30 |
+
| `repo` | _(empty)_ | Default Hugging Face repo id for new chats (e.g. `huggingface/transformers`). |
|
| 31 |
|
| 32 |
Example:
|
| 33 |
|
app.py
CHANGED
|
@@ -164,6 +164,7 @@ async def put_chat(chat_id: str, request: Request, x_user_id: str = Header(...))
|
|
| 164 |
"id": chat_id,
|
| 165 |
"title": body.get("title", "Untitled"),
|
| 166 |
"topic": body.get("topic"),
|
|
|
|
| 167 |
"updated_at": body.get("updated_at"),
|
| 168 |
},
|
| 169 |
)
|
|
|
|
| 164 |
"id": chat_id,
|
| 165 |
"title": body.get("title", "Untitled"),
|
| 166 |
"topic": body.get("topic"),
|
| 167 |
+
"repo": body.get("repo"),
|
| 168 |
"updated_at": body.get("updated_at"),
|
| 169 |
},
|
| 170 |
)
|
index.html
CHANGED
|
@@ -146,56 +146,35 @@
|
|
| 146 |
background: var(--bg);
|
| 147 |
}
|
| 148 |
.composer {
|
|
|
|
| 149 |
border: 1px solid var(--border);
|
| 150 |
border-radius: 22px;
|
| 151 |
background: var(--composer-bg);
|
| 152 |
-
padding: 14px 16px
|
| 153 |
display: flex;
|
| 154 |
-
|
| 155 |
-
gap:
|
| 156 |
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
| 157 |
}
|
| 158 |
.composer:focus-within { border-color: #c7c7cc; }
|
| 159 |
#input {
|
| 160 |
-
|
|
|
|
| 161 |
resize: none;
|
| 162 |
font: inherit;
|
| 163 |
color: inherit;
|
| 164 |
background: transparent;
|
| 165 |
border: 0;
|
| 166 |
outline: none;
|
| 167 |
-
padding: 4px
|
| 168 |
max-height: 240px;
|
| 169 |
min-height: 28px;
|
| 170 |
line-height: 1.45;
|
| 171 |
}
|
| 172 |
#input::placeholder { color: var(--muted); }
|
| 173 |
-
.toolbar {
|
| 174 |
-
display: flex;
|
| 175 |
-
align-items: center;
|
| 176 |
-
gap: 6px;
|
| 177 |
-
}
|
| 178 |
-
.toolbar .spacer { flex: 1; }
|
| 179 |
-
|
| 180 |
-
.pill {
|
| 181 |
-
appearance: none;
|
| 182 |
-
background: transparent;
|
| 183 |
-
border: 0;
|
| 184 |
-
color: var(--pill);
|
| 185 |
-
font: inherit;
|
| 186 |
-
font-weight: 500;
|
| 187 |
-
display: inline-flex;
|
| 188 |
-
align-items: center;
|
| 189 |
-
gap: 6px;
|
| 190 |
-
padding: 6px 10px;
|
| 191 |
-
margin-left: -8px;
|
| 192 |
-
border-radius: 999px;
|
| 193 |
-
cursor: pointer;
|
| 194 |
-
}
|
| 195 |
-
.pill:hover { background: rgba(37,99,235,0.08); }
|
| 196 |
-
.pill .chev { width: 14px; height: 14px; opacity: 0.85; }
|
| 197 |
|
| 198 |
#send {
|
|
|
|
| 199 |
background: var(--send-bg);
|
| 200 |
color: var(--send-fg);
|
| 201 |
width: 32px;
|
|
@@ -211,37 +190,6 @@
|
|
| 211 |
#send:disabled { opacity: 0.45; cursor: not-allowed; }
|
| 212 |
#send svg { width: 18px; height: 18px; }
|
| 213 |
|
| 214 |
-
/* Topic dropdown menu */
|
| 215 |
-
.menu-wrap { position: relative; }
|
| 216 |
-
.menu {
|
| 217 |
-
position: absolute;
|
| 218 |
-
bottom: calc(100% + 6px);
|
| 219 |
-
left: 0;
|
| 220 |
-
min-width: 220px;
|
| 221 |
-
background: var(--composer-bg);
|
| 222 |
-
border: 1px solid var(--border);
|
| 223 |
-
border-radius: 12px;
|
| 224 |
-
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
| 225 |
-
padding: 6px;
|
| 226 |
-
display: none;
|
| 227 |
-
z-index: 10;
|
| 228 |
-
}
|
| 229 |
-
.menu.open { display: block; }
|
| 230 |
-
.menu button {
|
| 231 |
-
display: block;
|
| 232 |
-
width: 100%;
|
| 233 |
-
text-align: left;
|
| 234 |
-
background: transparent;
|
| 235 |
-
border: 0;
|
| 236 |
-
color: inherit;
|
| 237 |
-
font: inherit;
|
| 238 |
-
padding: 8px 10px;
|
| 239 |
-
border-radius: 8px;
|
| 240 |
-
cursor: pointer;
|
| 241 |
-
}
|
| 242 |
-
.menu button:hover { background: rgba(127,127,127,0.12); }
|
| 243 |
-
.menu button.selected { color: var(--pill); font-weight: 500; }
|
| 244 |
-
|
| 245 |
/* ---- Tabs ---- */
|
| 246 |
.tabs {
|
| 247 |
display: flex;
|
|
@@ -548,22 +496,10 @@
|
|
| 548 |
|
| 549 |
<form id="form">
|
| 550 |
<div class="composer">
|
| 551 |
-
<textarea id="input" rows="1" placeholder="
|
| 552 |
-
<
|
| 553 |
-
<
|
| 554 |
-
|
| 555 |
-
<span id="topic-label">Continuous batching</span>
|
| 556 |
-
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
| 557 |
-
</button>
|
| 558 |
-
<div class="menu" id="topic-menu" role="listbox"></div>
|
| 559 |
-
</div>
|
| 560 |
-
|
| 561 |
-
<div class="spacer"></div>
|
| 562 |
-
|
| 563 |
-
<button id="send" type="submit" title="Send">
|
| 564 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
| 565 |
-
</button>
|
| 566 |
-
</div>
|
| 567 |
</div>
|
| 568 |
</form>
|
| 569 |
</main>
|
|
@@ -574,6 +510,7 @@
|
|
| 574 |
const _params = new URLSearchParams(location.search);
|
| 575 |
const BASE_URL = _params.get("api") || "http://localhost:8000/v1";
|
| 576 |
const MODEL = _params.get("model") || "Qwen/Qwen3-0.6B";
|
|
|
|
| 577 |
|
| 578 |
// Persistence: anonymous per-browser user id, used as X-User-Id against /api/chats.
|
| 579 |
function getUserId() {
|
|
@@ -781,9 +718,6 @@
|
|
| 781 |
const form = document.getElementById("form");
|
| 782 |
const input = document.getElementById("input");
|
| 783 |
const send = document.getElementById("send");
|
| 784 |
-
const topicBtn = document.getElementById("topic-btn");
|
| 785 |
-
const topicLabel = document.getElementById("topic-label");
|
| 786 |
-
const topicMenu = document.getElementById("topic-menu");
|
| 787 |
const newChatBtn = document.getElementById("new-chat");
|
| 788 |
const chatListEl = document.getElementById("chat-list");
|
| 789 |
|
|
@@ -796,8 +730,9 @@
|
|
| 796 |
const chat = {
|
| 797 |
id,
|
| 798 |
title: "New chat",
|
| 799 |
-
topic:
|
| 800 |
model: MODEL,
|
|
|
|
| 801 |
history: [],
|
| 802 |
};
|
| 803 |
chats.unshift(chat);
|
|
@@ -821,15 +756,14 @@
|
|
| 821 |
chat.history = Array.isArray(full.history) ? full.history : [];
|
| 822 |
chat.topic = full.topic || chat.topic;
|
| 823 |
chat.title = full.title || chat.title;
|
|
|
|
| 824 |
} else {
|
| 825 |
chat.history = [];
|
| 826 |
}
|
| 827 |
}
|
| 828 |
-
topicLabel.textContent = (TOPICS.find((t) => t.id === chat.topic) || TOPICS[0]).label;
|
| 829 |
log.innerHTML = "";
|
| 830 |
for (const m of chat.history) renderMsg(m.role, m.content);
|
| 831 |
renderChatList();
|
| 832 |
-
renderTopicMenu();
|
| 833 |
input.focus();
|
| 834 |
}
|
| 835 |
|
|
@@ -854,37 +788,14 @@
|
|
| 854 |
newChat();
|
| 855 |
});
|
| 856 |
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
const
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
b.textContent = t.label;
|
| 865 |
-
if (chat && t.id === chat.topic) b.classList.add("selected");
|
| 866 |
-
b.addEventListener("click", () => {
|
| 867 |
-
const c = getChat();
|
| 868 |
-
if (c) {
|
| 869 |
-
c.topic = t.id;
|
| 870 |
-
topicLabel.textContent = t.label;
|
| 871 |
-
if (Array.isArray(c.history) && c.history.length > 0) scheduleSave(c);
|
| 872 |
-
}
|
| 873 |
-
closeMenu();
|
| 874 |
-
renderTopicMenu();
|
| 875 |
-
});
|
| 876 |
-
topicMenu.appendChild(b);
|
| 877 |
-
});
|
| 878 |
}
|
| 879 |
-
function openMenu() { topicMenu.classList.add("open"); topicBtn.setAttribute("aria-expanded", "true"); }
|
| 880 |
-
function closeMenu() { topicMenu.classList.remove("open"); topicBtn.setAttribute("aria-expanded", "false"); }
|
| 881 |
-
topicBtn.addEventListener("click", (e) => {
|
| 882 |
-
e.stopPropagation();
|
| 883 |
-
topicMenu.classList.contains("open") ? closeMenu() : openMenu();
|
| 884 |
-
});
|
| 885 |
-
document.addEventListener("click", (e) => {
|
| 886 |
-
if (!topicMenu.contains(e.target) && e.target !== topicBtn) closeMenu();
|
| 887 |
-
});
|
| 888 |
|
| 889 |
// ---- Messages ----
|
| 890 |
function renderMsg(role, text) {
|
|
@@ -927,7 +838,7 @@
|
|
| 927 |
const botEl = renderMsg("bot", "");
|
| 928 |
|
| 929 |
const messages = [
|
| 930 |
-
{ role: "system", content:
|
| 931 |
...chat.history,
|
| 932 |
];
|
| 933 |
|
|
@@ -1338,7 +1249,8 @@
|
|
| 1338 |
chats = idx.map((e) => ({
|
| 1339 |
id: e.id,
|
| 1340 |
title: e.title || "Untitled",
|
| 1341 |
-
topic: e.topic ||
|
|
|
|
| 1342 |
history: null, // lazy-load on switch
|
| 1343 |
}));
|
| 1344 |
renderChatList();
|
|
|
|
| 146 |
background: var(--bg);
|
| 147 |
}
|
| 148 |
.composer {
|
| 149 |
+
--composer-pad-x: 2px;
|
| 150 |
border: 1px solid var(--border);
|
| 151 |
border-radius: 22px;
|
| 152 |
background: var(--composer-bg);
|
| 153 |
+
padding: 14px 16px;
|
| 154 |
display: flex;
|
| 155 |
+
align-items: flex-end;
|
| 156 |
+
gap: 10px;
|
| 157 |
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
| 158 |
}
|
| 159 |
.composer:focus-within { border-color: #c7c7cc; }
|
| 160 |
#input {
|
| 161 |
+
flex: 1;
|
| 162 |
+
min-width: 0;
|
| 163 |
resize: none;
|
| 164 |
font: inherit;
|
| 165 |
color: inherit;
|
| 166 |
background: transparent;
|
| 167 |
border: 0;
|
| 168 |
outline: none;
|
| 169 |
+
padding: 4px var(--composer-pad-x);
|
| 170 |
max-height: 240px;
|
| 171 |
min-height: 28px;
|
| 172 |
line-height: 1.45;
|
| 173 |
}
|
| 174 |
#input::placeholder { color: var(--muted); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
#send {
|
| 177 |
+
flex-shrink: 0;
|
| 178 |
background: var(--send-bg);
|
| 179 |
color: var(--send-fg);
|
| 180 |
width: 32px;
|
|
|
|
| 190 |
#send:disabled { opacity: 0.45; cursor: not-allowed; }
|
| 191 |
#send svg { width: 18px; height: 18px; }
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
/* ---- Tabs ---- */
|
| 194 |
.tabs {
|
| 195 |
display: flex;
|
|
|
|
| 496 |
|
| 497 |
<form id="form">
|
| 498 |
<div class="composer">
|
| 499 |
+
<textarea id="input" rows="1" placeholder="Pass in a repository (https://github.com/huggingface/transformers)" autofocus></textarea>
|
| 500 |
+
<button id="send" type="submit" title="Send">
|
| 501 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
| 502 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
</div>
|
| 504 |
</form>
|
| 505 |
</main>
|
|
|
|
| 510 |
const _params = new URLSearchParams(location.search);
|
| 511 |
const BASE_URL = _params.get("api") || "http://localhost:8000/v1";
|
| 512 |
const MODEL = _params.get("model") || "Qwen/Qwen3-0.6B";
|
| 513 |
+
const DEFAULT_REPO = _params.get("repo") || "";
|
| 514 |
|
| 515 |
// Persistence: anonymous per-browser user id, used as X-User-Id against /api/chats.
|
| 516 |
function getUserId() {
|
|
|
|
| 718 |
const form = document.getElementById("form");
|
| 719 |
const input = document.getElementById("input");
|
| 720 |
const send = document.getElementById("send");
|
|
|
|
|
|
|
|
|
|
| 721 |
const newChatBtn = document.getElementById("new-chat");
|
| 722 |
const chatListEl = document.getElementById("chat-list");
|
| 723 |
|
|
|
|
| 730 |
const chat = {
|
| 731 |
id,
|
| 732 |
title: "New chat",
|
| 733 |
+
topic: "general",
|
| 734 |
model: MODEL,
|
| 735 |
+
repo: DEFAULT_REPO,
|
| 736 |
history: [],
|
| 737 |
};
|
| 738 |
chats.unshift(chat);
|
|
|
|
| 756 |
chat.history = Array.isArray(full.history) ? full.history : [];
|
| 757 |
chat.topic = full.topic || chat.topic;
|
| 758 |
chat.title = full.title || chat.title;
|
| 759 |
+
chat.repo = full.repo ?? chat.repo ?? "";
|
| 760 |
} else {
|
| 761 |
chat.history = [];
|
| 762 |
}
|
| 763 |
}
|
|
|
|
| 764 |
log.innerHTML = "";
|
| 765 |
for (const m of chat.history) renderMsg(m.role, m.content);
|
| 766 |
renderChatList();
|
|
|
|
| 767 |
input.focus();
|
| 768 |
}
|
| 769 |
|
|
|
|
| 788 |
newChat();
|
| 789 |
});
|
| 790 |
|
| 791 |
+
function systemContent(chat) {
|
| 792 |
+
let content = TOPIC_PROMPTS[chat.topic] || TOPIC_PROMPTS.general;
|
| 793 |
+
const repo = (chat.repo || "").trim();
|
| 794 |
+
if (repo) {
|
| 795 |
+
content += `\n\nThe user is working in the Hugging Face repository \`${repo}\`. Ground answers in that repo when relevant (paths, APIs, conventions).`;
|
| 796 |
+
}
|
| 797 |
+
return content;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
|
| 800 |
// ---- Messages ----
|
| 801 |
function renderMsg(role, text) {
|
|
|
|
| 838 |
const botEl = renderMsg("bot", "");
|
| 839 |
|
| 840 |
const messages = [
|
| 841 |
+
{ role: "system", content: systemContent(chat) },
|
| 842 |
...chat.history,
|
| 843 |
];
|
| 844 |
|
|
|
|
| 1249 |
chats = idx.map((e) => ({
|
| 1250 |
id: e.id,
|
| 1251 |
title: e.title || "Untitled",
|
| 1252 |
+
topic: e.topic || "general",
|
| 1253 |
+
repo: e.repo || DEFAULT_REPO,
|
| 1254 |
history: null, // lazy-load on switch
|
| 1255 |
}));
|
| 1256 |
renderChatList();
|