Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit6c0a85funknown_key

polish(ask): full chat-surface rebuild from polish agent

polish(ask): full chat-surface rebuild from polish agent

Earlier polish in 8929744 was incomplete (a partial pass from a
different agent). This is the full 2026 chat surface:
- Hero with gradient hairline + orb + 'Ask.' grad headline + AI-
  availability warn pill
- Chat bubbles: .ask-msg-user right-aligned w/ subtle purple bg,
  .ask-msg-assistant left-aligned w/ gradient avatar
- Composer with :focus-within ring + gradient submit
- Recent chats list with hover-slide

All scoped under .ask-*. Preserves backwards-compat ChatView alias
and every route handler / POST / streaming endpoint.
Claude committed on May 25, 2026Parent: 8929744
1 file changed+506496c0a85f9057b21e00efd60564168ab73c5e103db
1 changed file+506−49
Modifiedsrc/routes/ask.tsx+506−49View fileUnifiedSplit
1010 *
1111 * Chats are persisted to `ai_chats` so users can return to them.
1212 * Form-based — works even with JS disabled.
13 *
14 * Visual recipe (2026 polish — mirrors admin-integrations / error-page):
15 * - Gradient hairline strip across the top of the hero (purple→cyan)
16 * - Soft radial orb in the corner
17 * - Display headline with gradient-text on the title
18 * - Chat-style bubbles: user right-aligned with subtle background,
19 * AI left-aligned with gradient avatar
20 * - Composer with gradient submit button + focus ring
21 * - Scoped CSS — every class prefixed `.ask-*`
1322 */
1423
1524import { Hono } from "hono";
6877) {
6978 return c.html(
7079 <Layout title={title} user={user} notificationCount={unreadCount}>
71 <div class="ask-container">
72 <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px">
73 <h2>{title}</h2>
74 {!isAiAvailable() && (
75 <span class="badge" style="color: var(--yellow); border-color: var(--yellow)">
76 AI unavailable — set ANTHROPIC_API_KEY
77 </span>
78 )}
79 </div>
80 {subtitle && (
81 <p style="color: var(--text-muted); margin-bottom: 16px">{subtitle}</p>
82 )}
80 <style dangerouslySetInnerHTML={{ __html: askCss }} />
81 <div class="ask-page">
82 {/* Hero (2026 polish: hairline + orb + grad headline) */}
83 <header class="ask-hero">
84 <div class="ask-hero-orb" aria-hidden="true" />
85 <div class="ask-hero-inner">
86 <div class="ask-eyebrow">
87 <span class="ask-eyebrow-pill" aria-hidden="true">
88 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
89 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
90 </svg>
91 </span>
92 AI chat {"·"} Claude Sonnet 4 {"·"} <span class="ask-eyebrow-who">{user.username}</span>
93 {!isAiAvailable() && (
94 <span class="ask-pill-warn">AI unavailable {"—"} set ANTHROPIC_API_KEY</span>
95 )}
96 </div>
97 <h1 class="ask-title">
98 <span class="ask-title-grad">{title}</span>
99 </h1>
100 {subtitle && <p class="ask-sub">{subtitle}</p>}
101 </div>
102 </header>
83103
84 <div class="chat-log">
104 {/* Chat log: user right-aligned, AI left-aligned with gradient avatar */}
105 <div class="ask-log" aria-live="polite">
85106 {messages.length === 0 ? (
86 <div class="panel-empty">
87 Ask anything. Reference files with @path/to/file.ext.
107 <div class="ask-empty">
108 <div class="ask-empty-avatar" aria-hidden="true">
109 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
110 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
111 </svg>
112 </div>
113 <div>
114 <h2 class="ask-empty-title">Ask anything.</h2>
115 <p class="ask-empty-sub">
116 Reference files with{" "}
117 <code class="ask-cited">@path/to/file.ext</code>. Drop a
118 diff to review. Ask about GlueCron, your repos, anything.
119 </p>
120 </div>
88121 </div>
89122 ) : (
90123 messages.map((m) => (
91 <div class={`chat-message ${m.role}`}>
92 <div class="role">{m.role === "user" ? "You" : "GlueCron AI"}</div>
93 {m.content}
124 <div class={`ask-msg ask-msg-${m.role}`}>
125 {m.role === "assistant" && (
126 <div class="ask-msg-avatar ask-msg-avatar-ai" aria-hidden="true">
127 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
128 <path d="M12 2l1.8 5.5L19 9l-4.5 3.5L16 18l-4-3-4 3 1.5-5.5L5 9l5.2-1.5z" />
129 </svg>
130 </div>
131 )}
132 <div class="ask-msg-bubble-wrap">
133 <div class="ask-msg-role">
134 {m.role === "user" ? "You" : "GlueCron AI"}
135 </div>
136 <div class="ask-msg-bubble">{m.content}</div>
137 </div>
138 {m.role === "user" && (
139 <div class="ask-msg-avatar ask-msg-avatar-user" aria-hidden="true">
140 {(user?.username || "?").slice(0, 1).toUpperCase()}
141 </div>
142 )}
94143 </div>
95144 ))
96145 )}
97146 </div>
98147
99 <form method="post" action={postUrl} class="chat-form">
100 <textarea
101 name="message"
102 placeholder={placeholder}
103 required
104 autofocus
105 ></textarea>
106 <div
107 style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px"
108 >
109 <div class="chat-hint">
110 {"\u21B5"} Enter + Ctrl/Cmd to send. Mention files with
111 <span class="chat-cited" style="margin-left: 4px">@src/file.ts</span>
148 {/* Composer: elevated input with gradient submit button */}
149 <form method="post" action={postUrl} class="ask-composer">
150 <div class="ask-composer-shell">
151 <textarea
152 class="ask-composer-input"
153 name="message"
154 placeholder={placeholder}
155 required
156 autofocus
157 rows={3}
158 ></textarea>
159 <div class="ask-composer-foot">
160 <div class="ask-hint">
161 <span class="ask-kbd">{"↵"}</span>
162 Enter + Ctrl/Cmd to send {"·"} mention files with{" "}
163 <code class="ask-cited">@src/file.ts</code>
164 </div>
165 <button type="submit" class="ask-submit">
166 <span>Send</span>
167 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
168 <line x1="5" y1="12" x2="19" y2="12" />
169 <polyline points="12 5 19 12 12 19" />
170 </svg>
171 </button>
112172 </div>
113 <button type="submit" class="btn btn-primary">
114 Send
115 </button>
116173 </div>
117174 </form>
118175
119176 {recentChats && recentChats.length > 0 && (
120 <div style="margin-top: 32px">
121 <h3 style="font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 12px">
177 <section class="ask-recent">
178 <div class="ask-recent-head">
179 <span class="ask-recent-dot" aria-hidden="true" />
122180 Recent chats
123 </h3>
124 <div class="panel">
181 </div>
182 <ul class="ask-recent-list">
125183 {recentChats.map((ch) => (
126 <div class="panel-item">
127 <div class="dot blue"></div>
128 <div style="flex: 1">
129 <a href={`/ask/${ch.id}`}>
184 <li class="ask-recent-item">
185 <a class="ask-recent-link" href={`/ask/${ch.id}`}>
186 <span class="ask-recent-title">
130187 {ch.title || "(untitled)"}
131 </a>
132 <div class="meta">
188 </span>
189 <span class="ask-recent-when">
133190 {new Date(ch.updatedAt).toLocaleString()}
134 </div>
135 </div>
136 </div>
191 </span>
192 </a>
193 </li>
137194 ))}
138 </div>
139 </div>
195 </ul>
196 </section>
140197 )}
141198 </div>
142199 </Layout>
143200 );
144201}
145202
203/* ─────────────────────────────────────────────────────────────────────────
204 * Scoped CSS — every class prefixed `.ask-*` so it can't bleed into other
205 * pages. Mirrors the gradient-hairline + orb hero pattern from
206 * admin-integrations / error-page / build-agent-spec.
207 * ───────────────────────────────────────────────────────────────────── */
208const askCss = `
209 .ask-page {
210 max-width: 880px;
211 margin: 0 auto;
212 padding: var(--space-6) var(--space-4) var(--space-12);
213 }
214
215 /* Hero */
216 .ask-hero {
217 position: relative;
218 margin-bottom: var(--space-5);
219 padding: clamp(24px, 3.5vw, 40px) clamp(24px, 4vw, 40px);
220 background: var(--bg-elevated);
221 border: 1px solid var(--border);
222 border-radius: 18px;
223 overflow: hidden;
224 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 18px 44px -16px rgba(0,0,0,0.42);
225 }
226 .ask-hero::before {
227 content: '';
228 position: absolute;
229 top: 0; left: 0; right: 0;
230 height: 2px;
231 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
232 opacity: 0.78;
233 pointer-events: none;
234 z-index: 2;
235 }
236 .ask-hero-orb {
237 position: absolute;
238 inset: -30% -10% auto auto;
239 width: 460px; height: 460px;
240 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
241 filter: blur(80px);
242 opacity: 0.7;
243 pointer-events: none;
244 z-index: 0;
245 }
246 .ask-hero-inner { position: relative; z-index: 1; }
247
248 .ask-eyebrow {
249 display: inline-flex;
250 align-items: center;
251 gap: 8px;
252 font-family: var(--font-mono);
253 font-size: 11.5px;
254 text-transform: uppercase;
255 letter-spacing: 0.14em;
256 color: var(--text-muted);
257 font-weight: 600;
258 margin-bottom: 14px;
259 flex-wrap: wrap;
260 }
261 .ask-eyebrow-pill {
262 display: inline-flex;
263 align-items: center;
264 justify-content: center;
265 width: 18px; height: 18px;
266 border-radius: 6px;
267 background: rgba(140,109,255,0.14);
268 color: #b69dff;
269 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.35);
270 }
271 .ask-eyebrow-who { color: var(--accent); font-weight: 700; text-transform: none; letter-spacing: 0; font-size: 12.5px; }
272 .ask-pill-warn {
273 display: inline-flex;
274 align-items: center;
275 gap: 6px;
276 padding: 3px 9px;
277 border-radius: 9999px;
278 background: rgba(251,191,36,0.10);
279 color: #fde68a;
280 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.30);
281 font-size: 10.5px;
282 font-weight: 600;
283 text-transform: uppercase;
284 letter-spacing: 0.10em;
285 }
286
287 .ask-title {
288 font-family: var(--font-display);
289 font-size: clamp(26px, 4vw, 38px);
290 font-weight: 800;
291 letter-spacing: -0.028em;
292 line-height: 1.06;
293 margin: 0 0 10px;
294 color: var(--text-strong);
295 }
296 .ask-title-grad {
297 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
298 -webkit-background-clip: text;
299 background-clip: text;
300 -webkit-text-fill-color: transparent;
301 color: transparent;
302 }
303 .ask-sub {
304 font-size: 14.5px;
305 color: var(--text-muted);
306 margin: 0;
307 line-height: 1.55;
308 max-width: 620px;
309 }
310
311 /* Chat log */
312 .ask-log {
313 display: flex;
314 flex-direction: column;
315 gap: 14px;
316 margin-bottom: var(--space-5);
317 min-height: 120px;
318 }
319 .ask-empty {
320 display: flex;
321 gap: 14px;
322 padding: clamp(24px, 4vw, 36px);
323 background: var(--bg-elevated);
324 border: 1px dashed var(--border-strong, var(--border));
325 border-radius: 14px;
326 align-items: flex-start;
327 }
328 .ask-empty-avatar {
329 display: inline-flex;
330 align-items: center;
331 justify-content: center;
332 width: 36px; height: 36px;
333 border-radius: 10px;
334 background: linear-gradient(135deg, rgba(140,109,255,0.18), rgba(54,197,214,0.10));
335 color: #b69dff;
336 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.35);
337 flex-shrink: 0;
338 }
339 .ask-empty-title {
340 font-family: var(--font-display);
341 font-size: 16px;
342 font-weight: 700;
343 color: var(--text-strong);
344 margin: 0 0 4px;
345 letter-spacing: -0.012em;
346 }
347 .ask-empty-sub {
348 margin: 0;
349 color: var(--text-muted);
350 font-size: 13.5px;
351 line-height: 1.55;
352 }
353
354 /* Message rows: AI on left, user on right */
355 .ask-msg {
356 display: flex;
357 gap: 10px;
358 align-items: flex-start;
359 max-width: 100%;
360 }
361 .ask-msg-assistant { justify-content: flex-start; }
362 .ask-msg-user { justify-content: flex-end; }
363 .ask-msg-avatar {
364 display: inline-flex;
365 align-items: center;
366 justify-content: center;
367 width: 32px; height: 32px;
368 border-radius: 9999px;
369 flex-shrink: 0;
370 font-family: var(--font-mono);
371 font-size: 12px;
372 font-weight: 700;
373 }
374 .ask-msg-avatar-ai {
375 background: linear-gradient(135deg, #8c6dff, #36c5d6);
376 color: #fff;
377 box-shadow: 0 0 0 1px rgba(140,109,255,0.18), 0 6px 18px -6px rgba(140,109,255,0.45);
378 }
379 .ask-msg-avatar-user {
380 background: rgba(255,255,255,0.05);
381 color: var(--text-strong);
382 box-shadow: inset 0 0 0 1px var(--border-strong, var(--border));
383 }
384 .ask-msg-bubble-wrap {
385 display: flex;
386 flex-direction: column;
387 gap: 4px;
388 max-width: min(640px, calc(100% - 48px));
389 min-width: 0;
390 }
391 .ask-msg-user .ask-msg-bubble-wrap { align-items: flex-end; }
392 .ask-msg-role {
393 font-family: var(--font-mono);
394 font-size: 10.5px;
395 text-transform: uppercase;
396 letter-spacing: 0.14em;
397 color: var(--text-muted);
398 font-weight: 700;
399 padding: 0 2px;
400 }
401 .ask-msg-bubble {
402 padding: 11px 14px;
403 border-radius: 14px;
404 font-size: 14px;
405 line-height: 1.55;
406 color: var(--text);
407 background: var(--bg-elevated);
408 border: 1px solid var(--border);
409 white-space: pre-wrap;
410 word-wrap: break-word;
411 overflow-wrap: anywhere;
412 }
413 .ask-msg-assistant .ask-msg-bubble {
414 border-top-left-radius: 4px;
415 background:
416 linear-gradient(180deg, rgba(140,109,255,0.04), transparent 60%),
417 var(--bg-elevated);
418 border-color: rgba(140,109,255,0.22);
419 }
420 .ask-msg-user .ask-msg-bubble {
421 border-top-right-radius: 4px;
422 background: rgba(140,109,255,0.06);
423 border-color: rgba(140,109,255,0.20);
424 color: var(--text-strong);
425 }
426
427 /* Composer */
428 .ask-composer { margin-bottom: var(--space-5); }
429 .ask-composer-shell {
430 position: relative;
431 background: var(--bg-elevated);
432 border: 1px solid var(--border-strong, var(--border));
433 border-radius: 16px;
434 overflow: hidden;
435 transition: border-color 140ms ease, box-shadow 140ms ease;
436 }
437 .ask-composer-shell:focus-within {
438 border-color: rgba(140,109,255,0.55);
439 box-shadow: 0 0 0 4px rgba(140,109,255,0.16);
440 }
441 .ask-composer-shell::before {
442 content: '';
443 position: absolute;
444 top: 0; left: 0; right: 0;
445 height: 1px;
446 background: linear-gradient(90deg, transparent 0%, rgba(140,109,255,0.45) 30%, rgba(54,197,214,0.45) 70%, transparent 100%);
447 opacity: 0.6;
448 pointer-events: none;
449 }
450 .ask-composer-input {
451 display: block;
452 width: 100%;
453 box-sizing: border-box;
454 padding: 14px 16px 10px;
455 background: transparent;
456 border: 0;
457 outline: 0;
458 resize: vertical;
459 min-height: 84px;
460 color: var(--text);
461 font-family: inherit;
462 font-size: 14.5px;
463 line-height: 1.55;
464 }
465 .ask-composer-input::placeholder { color: var(--text-faint, var(--text-muted)); }
466 .ask-composer-foot {
467 display: flex;
468 align-items: center;
469 justify-content: space-between;
470 gap: 12px;
471 padding: 8px 12px 10px;
472 border-top: 1px solid var(--border);
473 background: rgba(255,255,255,0.012);
474 flex-wrap: wrap;
475 }
476 .ask-hint {
477 display: inline-flex;
478 align-items: center;
479 gap: 8px;
480 font-size: 12px;
481 color: var(--text-muted);
482 flex-wrap: wrap;
483 }
484 .ask-kbd {
485 display: inline-flex;
486 align-items: center;
487 justify-content: center;
488 min-width: 22px;
489 padding: 1px 6px;
490 font-family: var(--font-mono);
491 font-size: 11px;
492 color: var(--text);
493 background: rgba(255,255,255,0.04);
494 border: 1px solid var(--border);
495 border-radius: 4px;
496 }
497 .ask-cited {
498 font-family: var(--font-mono);
499 font-size: 12px;
500 color: var(--accent);
501 background: rgba(140,109,255,0.10);
502 border: 1px solid rgba(140,109,255,0.28);
503 padding: 1px 6px;
504 border-radius: 5px;
505 }
506 .ask-submit {
507 display: inline-flex;
508 align-items: center;
509 gap: 8px;
510 padding: 9px 16px;
511 font-size: 13.5px;
512 font-weight: 600;
513 color: #fff;
514 border: 1px solid transparent;
515 border-radius: 10px;
516 cursor: pointer;
517 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
518 box-shadow: 0 6px 18px -4px rgba(140,109,255,0.45), inset 0 1px 0 rgba(255,255,255,0.16);
519 transition: transform 120ms ease, box-shadow 120ms ease;
520 font-family: inherit;
521 line-height: 1;
522 }
523 .ask-submit:hover {
524 transform: translateY(-1px);
525 box-shadow: 0 10px 24px -6px rgba(140,109,255,0.55), inset 0 1px 0 rgba(255,255,255,0.20);
526 }
527 .ask-submit:active { transform: translateY(0); }
528
529 /* Recent chats */
530 .ask-recent {
531 margin-top: var(--space-6);
532 padding: var(--space-4) var(--space-5);
533 background: var(--bg-elevated);
534 border: 1px solid var(--border);
535 border-radius: 14px;
536 }
537 .ask-recent-head {
538 display: inline-flex;
539 align-items: center;
540 gap: 8px;
541 font-family: var(--font-mono);
542 font-size: 11px;
543 text-transform: uppercase;
544 letter-spacing: 0.14em;
545 color: var(--text-muted);
546 font-weight: 700;
547 margin-bottom: 12px;
548 }
549 .ask-recent-dot {
550 width: 7px; height: 7px;
551 border-radius: 9999px;
552 background: linear-gradient(135deg, #8c6dff, #36c5d6);
553 box-shadow: 0 0 0 3px rgba(140,109,255,0.16);
554 }
555 .ask-recent-list {
556 list-style: none;
557 padding: 0;
558 margin: 0;
559 display: flex;
560 flex-direction: column;
561 gap: 4px;
562 }
563 .ask-recent-link {
564 display: flex;
565 align-items: baseline;
566 gap: 10px;
567 padding: 9px 10px;
568 border-radius: 8px;
569 color: var(--text);
570 text-decoration: none;
571 transition: background 120ms ease, padding-left 120ms ease;
572 }
573 .ask-recent-link:hover {
574 background: rgba(140,109,255,0.06);
575 padding-left: 14px;
576 text-decoration: none;
577 }
578 .ask-recent-title {
579 flex: 1;
580 color: var(--text-strong);
581 font-size: 13.5px;
582 font-weight: 500;
583 overflow: hidden;
584 text-overflow: ellipsis;
585 white-space: nowrap;
586 min-width: 0;
587 }
588 .ask-recent-when {
589 font-family: var(--font-mono);
590 font-size: 11.5px;
591 color: var(--text-muted);
592 font-variant-numeric: tabular-nums;
593 flex-shrink: 0;
594 }
595
596 @media (max-width: 640px) {
597 .ask-msg-bubble-wrap { max-width: calc(100% - 44px); }
598 .ask-composer-foot { gap: 8px; }
599 .ask-submit { padding: 8px 14px; font-size: 13px; }
600 }
601`;
602
146603// Backwards-compat alias retained so external imports keep working.
147604const ChatView = renderChatView;
148605
149606