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
+35 -23
View File
@@ -1,5 +1,5 @@
import { type Component, For, Show, createEffect, createMemo, createSignal } from "solid-js"; 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 { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FIREWORKS_HELP_MODEL } from "@/lib/app-config"; import { FIREWORKS_HELP_MODEL } from "@/lib/app-config";
@@ -146,36 +146,41 @@ export const AIHelpPanel: Component<{
<div <div
ref={messagesViewport} ref={messagesViewport}
class={cn( class={cn(
"min-h-0 flex-1 overflow-y-auto space-y-3 rounded-xl border border-border bg-background", "min-h-0 flex-1 overflow-y-auto space-y-3",
isSidebar() ? "p-2.5" : "p-3" isSidebar() ? "px-0.5" : "px-0.5"
)} )}
> >
<For each={messages()}> <For each={messages()}>
{(message) => ( {(message) => (
<div class="space-y-2"> <div class="space-y-2">
<div class={cn("flex", message.role === "user" ? "justify-end" : "justify-start")}>
<div <div
class={cn( class={cn(
"rounded-2xl px-4 py-3 leading-relaxed whitespace-pre-wrap shadow-sm", "flex",
isSidebar() ? "max-w-[92%]" : "max-w-[90%] sm:max-w-[80%]",
message.role === "user" message.role === "user"
? "bg-primary text-primary-foreground" ? "justify-center pt-8 pb-0"
: "bg-card border border-border text-foreground" : "justify-start"
)} )}
> >
<div
class={cn(
"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%]"
)
)}
>
<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"> <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>}>
<MessageCircle size={12} /> <MessageCircle size={12} />
<span>TasGrid Help</span> <span>TasGrid Help</span>
</Show>
</div> </div>
</Show>
<MarkdownMessage <MarkdownMessage
content={message.content} content={message.content}
class={cn( class=""
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"
: ""
)}
/> />
</div> </div>
</div> </div>
@@ -240,26 +245,33 @@ export const AIHelpPanel: Component<{
void submitQuestion(); void submitQuestion();
}} }}
> >
<div class="relative">
<textarea <textarea
value={input()} value={input()}
onInput={(e) => setInput(e.currentTarget.value)} 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..." placeholder="Ask how a TasGrid feature works, what a setting does, or why something may behave a certain way..."
class={cn( 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", "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" 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"
)} )}
/> />
<div class={cn(
"flex gap-2",
isSidebar() ? "flex-col" : "justify-end"
)}>
<Button <Button
type="submit" 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()} disabled={isLoading() || !input().trim()}
aria-label="Ask Question"
title="Ask Question"
> >
<Send size={14} /> <ArrowUp size={15} />
Ask Question
</Button> </Button>
</div> </div>
</form> </form>
+54
View File
@@ -20,6 +20,25 @@ const applyInlineMarkdown = (value: string) => {
return text; 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 renderMarkdown = (markdown: string) => {
const lines = markdown.replace(/\r\n/g, "\n").split("\n"); const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const html: string[] = []; const html: string[] = [];
@@ -46,6 +65,36 @@ const renderMarkdown = (markdown: string) => {
continue; 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)) { if (/^[-*]\s+/.test(trimmed)) {
const items: string[] = []; const items: string[] = [];
while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) { while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
@@ -97,12 +146,17 @@ export const MarkdownMessage: Component<{
<div <div
class={cn( class={cn(
"prose prose-sm dark:prose-invert max-w-none", "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-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-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: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-code:before:content-none prose-code:after:content-none",
"prose-a:text-primary prose-a:underline prose-a:underline-offset-4", "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-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 props.class
)} )}
innerHTML={html()} 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. If the guide does not contain the answer, say that the guide does not mention it.
Do not use outside product knowledge. Do not use outside product knowledge.
Do not invent buttons, workflows, settings, or behaviors. 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. When helpful, mention the relevant section name from the guide.
TASGRID REFERENCE GUIDE TASGRID REFERENCE GUIDE