improved chat ui
This commit is contained in:
@@ -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(
|
||||
"rounded-2xl px-4 py-3 leading-relaxed whitespace-pre-wrap shadow-sm",
|
||||
isSidebar() ? "max-w-[92%]" : "max-w-[90%] sm:max-w-[80%]",
|
||||
"flex",
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card border border-border text-foreground"
|
||||
? "justify-center pt-8 pb-0"
|
||||
: "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">
|
||||
<Show when={message.role === "assistant"} fallback={<span>You</span>}>
|
||||
<MessageCircle size={12} />
|
||||
<span>TasGrid Help</span>
|
||||
</Show>
|
||||
</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();
|
||||
}}
|
||||
>
|
||||
<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 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
|
||||
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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user