diff --git a/components/elements/ColorSchemeSelector.tsx b/components/elements/ColorSchemeSelector.tsx index 195a45d..c111eee 100644 --- a/components/elements/ColorSchemeSelector.tsx +++ b/components/elements/ColorSchemeSelector.tsx @@ -1,6 +1,18 @@ import React, { type DetailedHTMLProps, type HTMLAttributes } from "react"; import { twMerge } from "tailwind-merge"; import { Moon, Sun } from "lucide-react"; +import type { TWUIButtonSizeType } from "../layout/Button"; + +const SizeMap: Record< + TWUIButtonSizeType, + { icon: number; className: string } +> = { + smaller: { icon: 16, className: "w-4 h-4" }, + small: { icon: 20, className: "w-5 h-5" }, + normal: { icon: 24, className: "w-6 h-6" }, + large: { icon: 28, className: "w-7 h-7" }, + larger: { icon: 32, className: "w-8 h-8" }, +}; type Props = DetailedHTMLProps< HTMLAttributes, @@ -13,6 +25,11 @@ type Props = DetailedHTMLProps< HTMLDivElement >; defaultScheme?: "light" | "dark"; + /** + * Icon size. Matches the twui `Button` size scale. + * @default "normal" + */ + size?: TWUIButtonSizeType; }; /** @@ -24,8 +41,10 @@ export default function ColorSchemeSelector({ setActive: externalSetActive, iconWrapperProps, defaultScheme, + size = "normal", ...props }: Props) { + const { icon: iconSize, className: sizeClassName } = SizeMap[size]; const [active, setActive] = React.useState(initialActive); React.useEffect(() => { @@ -49,7 +68,7 @@ export default function ColorSchemeSelector({ } else if (existingTheme === "light") { setActive(false); } else if (defaultScheme) { - setActive(defaultScheme == "dark" ? false : true); + setActive(defaultScheme == "dark"); } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { setActive(true); } else if (typeof active == "undefined") { @@ -60,15 +79,21 @@ export default function ColorSchemeSelector({ React.useEffect(() => { if (typeof active == "undefined") return; - if (active) { - document.documentElement.className = "dark"; - localStorage.setItem("theme", "dark"); - } else { - document.documentElement.className = "light"; - localStorage.setItem("theme", "light"); - } + document.documentElement.classList.remove("dark", "light"); + document.documentElement.classList.add(active ? "dark" : "light"); }, [active]); + /** + * Only an explicit toggle is persisted, so visitors who never choose + * keep following their OS preference. + */ + function toggleScheme() { + const next = !active; + setActive(next); + externalSetActive?.(next); + localStorage.setItem("theme", next ? "dark" : "light"); + } + return (
diff --git a/components/elements/ai/AIPromptActionSection.tsx b/components/elements/ai/AIPromptActionSection.tsx index 0b0029c..4a8144f 100644 --- a/components/elements/ai/AIPromptActionSection.tsx +++ b/components/elements/ai/AIPromptActionSection.tsx @@ -6,22 +6,20 @@ import Button from "../../layout/Button"; import CopySlug from "../CopySlug"; import AIPromptHistoryModal from "./AIPromptHistoryModal"; import AIVoiceTranscription from "./AIVoiceTranscription"; +import type { VoiceTranscriptionParams } from "./types"; -type Props = { +type Props = VoiceTranscriptionParams & { streamRes: string; setStreamRes: Dispatch>; setPrompt: Dispatch>; loading: boolean; promptFn: (prompt: string) => void; - voice_transcription_fn?: (params: { last_message?: string }) => void; - transcribing?: boolean; history: ChatCompletionMessageParam[]; prompt: string; currentPromptRef: React.MutableRefObject; promptInputRef: React.RefObject; user_image_url?: string; last_response?: string; - cancel_voice_transcription_fn?: () => void; }; export default function AIPromptActionSection({ @@ -77,6 +75,11 @@ export default function AIPromptActionSection({ diff --git a/components/elements/ai/AIPromptHistoryAssistantCard.tsx b/components/elements/ai/AIPromptHistoryAssistantCard.tsx new file mode 100644 index 0000000..4688971 --- /dev/null +++ b/components/elements/ai/AIPromptHistoryAssistantCard.tsx @@ -0,0 +1,77 @@ +import type { ChatCompletionMessageParam } from "openai/resources/index"; +import { twMerge } from "tailwind-merge"; +import { Bot, User } from "lucide-react"; +import Stack from "../../layout/Stack"; +import Row from "../../layout/Row"; +import Card from "../Card"; +import Border from "../Border"; +import Img from "../../layout/Img"; +import type { VoiceTranscriptionParams } from "./types"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import AIVoiceTranscription from "./AIVoiceTranscription"; +import Span from "../../layout/Span"; +import CopySlug from "../CopySlug"; +import EmptyContent from "../EmptyContent"; + +type Props = VoiceTranscriptionParams & { + history: ChatCompletionMessageParam; + user_image_url?: string; +}; + +export default function AIPromptHistoryAssistantCard({ + history, + user_image_url, + cancel_voice_transcription_fn, + transcribing, + voice_transcription_fn, +}: Props) { + const text_content = history.content?.toString(); + + if (!text_content) { + return ; + } + + return ( + + + + + + + + + + + + {history.content?.toString()} + + + {" "} + + + + + + + + ); +} diff --git a/components/elements/ai/AIPromptHistoryModal.tsx b/components/elements/ai/AIPromptHistoryModal.tsx index a5a1804..73552f5 100644 --- a/components/elements/ai/AIPromptHistoryModal.tsx +++ b/components/elements/ai/AIPromptHistoryModal.tsx @@ -1,21 +1,16 @@ import type { ChatCompletionMessageParam } from "openai/resources/index"; -import { twMerge } from "tailwind-merge"; -import { Bot, History, User } from "lucide-react"; +import { History } from "lucide-react"; import Modal from "../Modal"; import Button from "../../layout/Button"; import Stack from "../../layout/Stack"; import H2 from "../../layout/H2"; import Span from "../../layout/Span"; import Divider from "../../layout/Divider"; -import Row from "../../layout/Row"; -import Card from "../Card"; -import Border from "../Border"; +import type { VoiceTranscriptionParams } from "./types"; +import AIPromptHistoryUserCard from "./AIPromptHistoryUserCard"; +import AIPromptHistoryAssistantCard from "./AIPromptHistoryAssistantCard"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import Img from "../../layout/Img"; - -type Props = { +type Props = VoiceTranscriptionParams & { history: ChatCompletionMessageParam[]; user_image_url?: string; }; @@ -23,6 +18,7 @@ type Props = { export default function AIPromptHistoryModal({ history, user_image_url, + ...props }: Props) { if (!history[0]) return null; @@ -54,60 +50,22 @@ export default function AIPromptHistoryModal({ {history.map((hst, index) => { if (hst.role == "user") { return ( - - - - {hst.content?.toString()} - - - - {user_image_url ? ( - Prompting user - ) : ( - - - - )} - + user_image_url={user_image_url} + /> ); } return ( - - - - - - - - - - {hst.content?.toString()} - - - - + user_image_url={user_image_url} + /> ); })} diff --git a/components/elements/ai/AIPromptHistoryUserCard.tsx b/components/elements/ai/AIPromptHistoryUserCard.tsx new file mode 100644 index 0000000..241ac5e --- /dev/null +++ b/components/elements/ai/AIPromptHistoryUserCard.tsx @@ -0,0 +1,48 @@ +import type { ChatCompletionMessageParam } from "openai/resources/index"; +import { twMerge } from "tailwind-merge"; +import { User } from "lucide-react"; +import Stack from "../../layout/Stack"; +import Row from "../../layout/Row"; +import Card from "../Card"; +import Border from "../Border"; +import Img from "../../layout/Img"; +import type { VoiceTranscriptionParams } from "./types"; + +type Props = VoiceTranscriptionParams & { + history: ChatCompletionMessageParam; + user_image_url?: string; +}; + +export default function AIPromptHistoryUserCard({ + history, + user_image_url, + ...props +}: Props) { + return ( + + + + {history.content?.toString()} + + + + {user_image_url ? ( + Prompting user + ) : ( + + + + )} + + ); +} diff --git a/components/elements/ai/AIVoiceTranscription.tsx b/components/elements/ai/AIVoiceTranscription.tsx index 58184d5..8298131 100644 --- a/components/elements/ai/AIVoiceTranscription.tsx +++ b/components/elements/ai/AIVoiceTranscription.tsx @@ -1,6 +1,6 @@ import { Speech, Voicemail, X } from "lucide-react"; import Button from "../../layout/Button"; -import { ChatCompletionMessageParam } from "openai/resources"; +import type { ChatCompletionMessageParam } from "openai/resources"; import Row from "../../layout/Row"; import Loading from "../Loading"; import Border from "../Border"; diff --git a/components/elements/ai/types.ts b/components/elements/ai/types.ts new file mode 100644 index 0000000..38d919a --- /dev/null +++ b/components/elements/ai/types.ts @@ -0,0 +1,5 @@ +export type VoiceTranscriptionParams = { + transcribing?: boolean; + cancel_voice_transcription_fn?: () => void; + voice_transcription_fn?: (params: { last_message?: string }) => void; +}; diff --git a/components/form/Input/index.tsx b/components/form/Input/index.tsx index 13f66a8..bf1e893 100644 --- a/components/form/Input/index.tsx +++ b/components/form/Input/index.tsx @@ -489,8 +489,8 @@ export default function Input( {info && ( - + + { setImageError(true);