improved chat ui
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user