improved chat ui
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m34s

This commit is contained in:
2026-03-26 10:46:26 -05:00
parent e45bc9e2b6
commit 613a2c72f5
3 changed files with 102 additions and 35 deletions
+46 -34
View File
@@ -1,5 +1,5 @@
import { type Component, For, Show, createEffect, createMemo, createSignal } from "solid-js";
import { MessageCircle, LoaderCircle, RefreshCw, Send } from "lucide-solid";
import { MessageCircle, LoaderCircle, RefreshCw, ArrowUp } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { FIREWORKS_HELP_MODEL } from "@/lib/app-config";
@@ -146,36 +146,41 @@ export const AIHelpPanel: Component<{
<div
ref={messagesViewport}
class={cn(
"min-h-0 flex-1 overflow-y-auto space-y-3 rounded-xl border border-border bg-background",
isSidebar() ? "p-2.5" : "p-3"
"min-h-0 flex-1 overflow-y-auto space-y-3",
isSidebar() ? "px-0.5" : "px-0.5"
)}
>
<For each={messages()}>
{(message) => (
<div class="space-y-2">
<div class={cn("flex", message.role === "user" ? "justify-end" : "justify-start")}>
<div
class={cn(
"flex",
message.role === "user"
? "justify-center pt-8 pb-0"
: "justify-start"
)}
>
<div
class={cn(
"rounded-2xl px-4 py-3 leading-relaxed whitespace-pre-wrap shadow-sm",
isSidebar() ? "max-w-[92%]" : "max-w-[90%] sm:max-w-[80%]",
message.role === "user"
? "bg-primary text-primary-foreground"
: "bg-card border border-border text-foreground"
"rounded-2xl px-4 leading-relaxed whitespace-pre-wrap shadow-sm",
message.role === "assistant"
? "w-full bg-card border border-border px-4 py-3 text-foreground"
: cn(
"bg-card px-4 pt-3 pb-0 text-foreground",
isSidebar() ? "max-w-[92%]" : "max-w-[90%] sm:max-w-[80%]"
)
)}
>
<div class="mb-1 flex items-center gap-2 text-[0.625rem] font-black uppercase tracking-[0.18em] opacity-70">
<Show when={message.role === "assistant"} fallback={<span>You</span>}>
<Show when={message.role === "assistant"}>
<div class="mb-1 flex items-center gap-2 text-[0.625rem] font-black uppercase tracking-[0.18em] opacity-70">
<MessageCircle size={12} />
<span>TasGrid Help</span>
</Show>
</div>
</div>
</Show>
<MarkdownMessage
content={message.content}
class={cn(
message.role === "user"
? "prose-invert prose-headings:text-primary-foreground prose-p:text-primary-foreground prose-strong:text-primary-foreground prose-code:bg-primary-foreground/15 prose-code:text-primary-foreground prose-a:text-primary-foreground prose-blockquote:text-primary-foreground"
: ""
)}
class=""
/>
</div>
</div>
@@ -240,26 +245,33 @@ export const AIHelpPanel: Component<{
void submitQuestion();
}}
>
<textarea
value={input()}
onInput={(e) => setInput(e.currentTarget.value)}
placeholder="Ask how a TasGrid feature works, what a setting does, or why something may behave a certain way..."
class={cn(
"flex w-full rounded-xl border border-input bg-background transition-colors placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none leading-relaxed",
isSidebar() ? "min-h-[88px] px-3 py-2.5 text-sm" : "min-h-[96px] px-3 py-2.5 text-sm"
)}
/>
<div class={cn(
"flex gap-2",
isSidebar() ? "flex-col" : "justify-end"
)}>
<div class="relative">
<textarea
value={input()}
onInput={(e) => setInput(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void submitQuestion();
}
}}
placeholder="Ask how a TasGrid feature works, what a setting does, or why something may behave a certain way..."
class={cn(
"flex w-full rounded-xl border border-input bg-background transition-colors placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none leading-relaxed",
isSidebar()
? "min-h-[88px] px-3 py-2.5 pr-14 text-sm"
: "min-h-[96px] px-3 py-2.5 pr-14 text-sm"
)}
/>
<Button
type="submit"
class={cn("gap-2 rounded-xl", isSidebar() && "w-full")}
size="icon"
class="absolute bottom-3 right-3 h-8 w-8 rounded-full shadow-sm"
disabled={isLoading() || !input().trim()}
aria-label="Ask Question"
title="Ask Question"
>
<Send size={14} />
Ask Question
<ArrowUp size={15} />
</Button>
</div>
</form>
+54
View File
@@ -20,6 +20,25 @@ const applyInlineMarkdown = (value: string) => {
return text;
};
const splitTableRow = (line: string) => {
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
return trimmed.split("|").map(cell => applyInlineMarkdown(cell.trim()));
};
const isTableSeparator = (line: string) => {
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
if (!trimmed) return false;
return trimmed
.split("|")
.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
};
const isTableRow = (line: string) => {
const trimmed = line.trim();
return trimmed.includes("|") && splitTableRow(trimmed).length > 1;
};
const renderMarkdown = (markdown: string) => {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const html: string[] = [];
@@ -46,6 +65,36 @@ const renderMarkdown = (markdown: string) => {
continue;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const text = applyInlineMarkdown(headingMatch[2].trim());
html.push(`<h${level}>${text}</h${level}>`);
i += 1;
continue;
}
if (
i + 1 < lines.length &&
isTableRow(lines[i]) &&
isTableSeparator(lines[i + 1])
) {
const headerCells = splitTableRow(lines[i]);
const bodyRows: string[] = [];
i += 2;
while (i < lines.length && lines[i].trim() && isTableRow(lines[i])) {
const cells = splitTableRow(lines[i]).map(cell => `<td>${cell}</td>`);
bodyRows.push(`<tr>${cells.join("")}</tr>`);
i += 1;
}
html.push(
`<div class="table-wrap"><table><thead><tr>${headerCells.map(cell => `<th>${cell}</th>`).join("")}</tr></thead>${bodyRows.length > 0 ? `<tbody>${bodyRows.join("")}</tbody>` : ""}</table></div>`
);
continue;
}
if (/^[-*]\s+/.test(trimmed)) {
const items: string[] = [];
while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
@@ -97,12 +146,17 @@ export const MarkdownMessage: Component<{
<div
class={cn(
"prose prose-sm dark:prose-invert max-w-none",
"prose-headings:mt-4 prose-headings:mb-2 prose-headings:font-semibold prose-headings:tracking-tight",
"prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-h4:text-sm",
"prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-0",
"prose-pre:my-3 prose-pre:rounded-lg prose-pre:bg-muted prose-pre:p-3",
"prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-[0.9em]",
"prose-code:before:content-none prose-code:after:content-none",
"prose-a:text-primary prose-a:underline prose-a:underline-offset-4",
"prose-blockquote:border-l-4 prose-blockquote:border-primary prose-blockquote:pl-4 prose-blockquote:italic",
"prose-table:my-3 prose-table:w-full prose-table:border-collapse prose-th:border prose-th:border-border prose-th:bg-muted prose-th:px-2 prose-th:py-1 prose-th:text-left prose-th:font-semibold",
"prose-td:border prose-td:border-border prose-td:px-2 prose-td:py-1 align-top",
"[&_div.table-wrap]:my-3 [&_div.table-wrap]:overflow-x-auto",
props.class
)}
innerHTML={html()}
+2 -1
View File
@@ -13,7 +13,8 @@ You must follow this instruction exactly: "use only this guide for all informati
If the guide does not contain the answer, say that the guide does not mention it.
Do not use outside product knowledge.
Do not invent buttons, workflows, settings, or behaviors.
Prefer concise, practical answers.
Answer in the most direct and concise way possible initially.
After the main answer, end with a short follow-up line like "If you would like, I can explain the intricacies in more detail."
When helpful, mention the relevant section name from the guide.
TASGRID REFERENCE GUIDE