mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-11 12:50:34 +08:00
Restructure web components
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { ProcessText, ShowCitation, ShowInTextCitation } from "./citations";
|
||||
import { GlossarySpan } from "./glossary";
|
||||
import type { Citation, AssistantEntry } from "../types";
|
||||
|
||||
export const ShowAssistantEntry: React.FC<{entry: AssistantEntry}> = ({entry}) => {
|
||||
const in_text_citation_regex = /\[([0-9]+)\]/g;
|
||||
|
||||
let [response, cite_map] = ProcessText(entry.content, entry.base_count);
|
||||
|
||||
// ----------------- create the ordered citation array -----------------
|
||||
|
||||
const citations = new Map<number, Citation>();
|
||||
cite_map.forEach((value, key) => {
|
||||
const index = key.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
if (index >= entry.citations.length) {
|
||||
console.log("invalid citation index: " + index);
|
||||
} else {
|
||||
citations.set(value, entry.citations[index]!);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-3 mb-8">
|
||||
{ // split into paragraphs
|
||||
response.split("\n").map(paragraph => ( <p> {
|
||||
paragraph.split(in_text_citation_regex).map((text, i) => {
|
||||
if (i % 2 === 0) {
|
||||
return <GlossarySpan content={text.trim()} />;
|
||||
}
|
||||
i = parseInt(text) - 1;
|
||||
if (!citations.has(i)) return `[${text}]`;
|
||||
const citation = citations.get(i)!;
|
||||
return (
|
||||
<ShowInTextCitation citation={citation} i={i} />
|
||||
);
|
||||
})
|
||||
} </p>))
|
||||
}
|
||||
<ul className="mt-5">
|
||||
{ // show citations
|
||||
Array.from(citations.entries()).map(([i, citation]) => (
|
||||
<li key={i}>
|
||||
<ShowCitation citation={citation} i={i} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { Citation } from "../types";
|
||||
import { Colours, A } from "./html";
|
||||
|
||||
|
||||
// todo: memoize this if too slow.
|
||||
export const ProcessText: (text: string, base_count: number) => [string, Map<string, number>] = (text, base_count) => {
|
||||
|
||||
// ---------------------- normalize citation form ----------------------
|
||||
// the general plan here is just to add parsing cases until we can respond
|
||||
// well to almost everything the LLM emits. We won't ever reach five nines,
|
||||
// but the domain is one where occasionally failing isn't catastrophic.
|
||||
|
||||
// transform all things that look like [a, b, c] into [a][b][c]
|
||||
let response = text.replace(
|
||||
|
||||
/\[((?:[a-z]+,\s*)*[a-z]+)\]/g, // identify groups of this form
|
||||
|
||||
(block: string) => block.split(',')
|
||||
.map((x) => x.trim())
|
||||
.join("][")
|
||||
)
|
||||
|
||||
// transform all things that look like [(a), (b), (c)] into [(a)][(b)][(c)]
|
||||
response = response.replace(
|
||||
|
||||
/\[((?:\([a-z]+\),\s*)*\([a-z]+\))\]/g, // identify groups of this form
|
||||
|
||||
(block: string) => block.split(',')
|
||||
.map((x) => x.trim())
|
||||
.join("][")
|
||||
)
|
||||
|
||||
// transform all things that look like [(a)] into [a]
|
||||
response = response.replace(
|
||||
/\[\(([a-z]+)\)\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
)
|
||||
|
||||
// transform all things that look like [ a ] into [a]
|
||||
response = response.replace(
|
||||
/\[\s*([a-z]+)\s*\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
)
|
||||
|
||||
// -------------- map citations from strings into numbers --------------
|
||||
|
||||
// figure out what citations are in the response, and map them appropriately
|
||||
const cite_map = new Map<string, number>();
|
||||
let cite_count = 0;
|
||||
|
||||
// scan a regex for [x] over the response. If x isn't in the map, add it.
|
||||
// (note: we're actually doing this twice - once on parsing, once on render.
|
||||
// if that looks like a problem, we could swap from strings to custom ropes).
|
||||
const regex = /\[([a-z]+)\]/g;
|
||||
let match;
|
||||
let response_copy = ""
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
if (!cite_map.has(match[1]!)) {
|
||||
cite_map.set(match[1]!, base_count + cite_count++);
|
||||
}
|
||||
// replace [x] with [i]
|
||||
response_copy += response.slice(response_copy.length, match.index) + `[${cite_map.get(match[1]!)! + 1}]`;
|
||||
}
|
||||
|
||||
response = response_copy + response.slice(response_copy.length);
|
||||
|
||||
return [response, cite_map]
|
||||
}
|
||||
|
||||
export const ShowCitation: React.FC<{citation: Citation, i: number}> = ({citation, i}) => {
|
||||
|
||||
var c_str = citation.title;
|
||||
|
||||
if (citation.authors && citation.authors.length > 0)
|
||||
c_str += " - " + citation.authors.join(', ');
|
||||
if (citation.date && citation.date !== "")
|
||||
c_str += " - " + citation.date;
|
||||
|
||||
// if we don't have a url, link to a duckduckgo search for the title instead
|
||||
const url = citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
|
||||
return (
|
||||
<A className={Colours[i % Colours.length] + " border-2 flex items-center rounded my-2 text-sm no-underline w-fit"}
|
||||
href={url}>
|
||||
<span className="mx-1"> [{i + 1}] </span>
|
||||
<p className="mx-1 my-0"> {c_str} </p>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowInTextCitation: React.FC<{citation: Citation, i: number}> = ({citation, i}) => {
|
||||
const url = citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
return (
|
||||
<A className={Colours[i % Colours.length] + " border-2 rounded text-sm no-underline w-min px-0.5 pb-0.5 ml-1 mr-0.5"}
|
||||
href={url}>
|
||||
[{i + 1}]
|
||||
</A>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
export type Mode = "rookie" | "concise" | "default";
|
||||
|
||||
export const Controls = ({
|
||||
mode,
|
||||
setMode,
|
||||
}: {
|
||||
mode: [Mode, boolean];
|
||||
setMode: (m: any) => void;
|
||||
}) => {
|
||||
{
|
||||
/* three buttons for the three modes, place far right, 1rem between each */
|
||||
}
|
||||
return (
|
||||
<div className="ml-auto mr-0 mb-5 flex w-fit flex-row justify-center gap-2">
|
||||
<button
|
||||
className={
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "rookie" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(["rookie", true]);
|
||||
}}
|
||||
title="For people who are new to the field of AI alignment. The
|
||||
answer might be longer, since technical terms will be
|
||||
explained in more detail and less background will be
|
||||
assumed."
|
||||
>
|
||||
rookie
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "concise" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(["concise", true]);
|
||||
}}
|
||||
title="Quick and to the point. Followup questions may need to be
|
||||
asked to get the full picture of what's going on."
|
||||
>
|
||||
concise
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "default" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(["default", true]);
|
||||
}}
|
||||
title="A balanced default mode."
|
||||
>
|
||||
default
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from 'next/image';
|
||||
import logo from "./logo.svg"
|
||||
import logo from "../logo.svg"
|
||||
|
||||
const Header: React.FC<{page: "index" | "semantic"}> = ({page}) => {
|
||||
const sidebar = page === "index" ? (
|
||||
@@ -0,0 +1,31 @@
|
||||
// const Colours = ["blue", "cyan", "teal", "green", "amber"].map(
|
||||
// colour => `bg-${colour}-100 border-${colour}-300 text-${colour}-800`
|
||||
// );
|
||||
// this would be nice, but Tailwind needs te actual string of the class to be in
|
||||
// the source file for it to be included in the build
|
||||
|
||||
export const Colours = [
|
||||
"bg-red-100 border-red-300 text-red-800",
|
||||
"bg-amber-100 border-amber-300 text-amber-800",
|
||||
"bg-orange-100 border-orange-300 text-orange-800",
|
||||
"bg-lime-100 border-lime-300 text-lime-800",
|
||||
"bg-green-100 border-green-300 text-green-800",
|
||||
"bg-cyan-100 border-cyan-300 text-cyan-800",
|
||||
"bg-blue-100 border-blue-300 text-blue-800",
|
||||
"bg-violet-100 border-violet-300 text-violet-800",
|
||||
"bg-pink-100 border-pink-300 text-pink-800",
|
||||
];
|
||||
|
||||
|
||||
export const A: React.FC<{href: string, className?: string, children: React.ReactNode}> = ({href, className, children}) => {
|
||||
// link element that only populates the href field if the contents are there
|
||||
return href && href !== "" ? (
|
||||
<a className={className} href={href} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<a className={className}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Followup } from "../types";
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export type Followup = {
|
||||
text: string;
|
||||
pageid: string;
|
||||
score: number;
|
||||
}
|
||||
import type { Followup } from "../types";
|
||||
|
||||
// initial questions to fill the search box with.
|
||||
export const initialQuestions: string[] = [
|
||||
@@ -120,4 +116,3 @@ const SearchBoxInternal: React.FC<{search: (
|
||||
export const SearchBox = dynamic(() => Promise.resolve(SearchBoxInternal), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import "~/styles/globals.css";
|
||||
|
||||
import { Glossary, GlossaryContext } from "../glossary";
|
||||
import { Glossary, GlossaryContext } from "../components/glossary";
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
const [glossary, setGlossary] = useState<{ g: Glossary, r: RegExp } | null>(null);
|
||||
|
||||
+8
-252
@@ -5,235 +5,18 @@ import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from 'next/image';
|
||||
|
||||
import Header from "../header";
|
||||
import { SearchBox, Followup } from "../searchbox";
|
||||
import Header from "../components/header";
|
||||
import logo from "../logo.svg"
|
||||
import { GlossarySpan } from "../glossary";
|
||||
import { API_URL } from "../settings"
|
||||
|
||||
type Citation = {
|
||||
title: string;
|
||||
authors: string[];
|
||||
date: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
|
||||
type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage;
|
||||
|
||||
type UserEntry = {
|
||||
role: "user";
|
||||
content: string;
|
||||
}
|
||||
|
||||
type AssistantEntry = {
|
||||
role: "assistant";
|
||||
content: string;
|
||||
citations: Citation[];
|
||||
base_count: number; // the number to start counting citations at
|
||||
}
|
||||
|
||||
type ErrorMessage = {
|
||||
role: "error";
|
||||
content: string;
|
||||
}
|
||||
|
||||
type StampyMessage = {
|
||||
role: "stampy";
|
||||
content: string;
|
||||
url: string;
|
||||
}
|
||||
import type { Citation, Entry, UserEntry, AssistantEntry, ErrorMessage, StampyMessage } from "../types";
|
||||
import { SearchBox, Followup } from "../components/searchbox";
|
||||
import { GlossarySpan } from "../components/glossary";
|
||||
import { Controls, Mode } from "../components/controls";
|
||||
import { ShowAssistantEntry } from "../components/assistant";
|
||||
import { ProcessText } from "../components/citations";
|
||||
|
||||
const MAX_FOLLOWUPS = 4;
|
||||
|
||||
// const Colours = ["blue", "cyan", "teal", "green", "amber"].map(
|
||||
// colour => `bg-${colour}-100 border-${colour}-300 text-${colour}-800`
|
||||
// );
|
||||
// this would be nice, but Tailwind needs te actual string of the class to be in
|
||||
// the source file for it to be included in the build
|
||||
|
||||
const Colours = [
|
||||
"bg-red-100 border-red-300 text-red-800",
|
||||
"bg-amber-100 border-amber-300 text-amber-800",
|
||||
"bg-orange-100 border-orange-300 text-orange-800",
|
||||
"bg-lime-100 border-lime-300 text-lime-800",
|
||||
"bg-green-100 border-green-300 text-green-800",
|
||||
"bg-cyan-100 border-cyan-300 text-cyan-800",
|
||||
"bg-blue-100 border-blue-300 text-blue-800",
|
||||
"bg-violet-100 border-violet-300 text-violet-800",
|
||||
"bg-pink-100 border-pink-300 text-pink-800",
|
||||
];
|
||||
|
||||
const ShowCitation: React.FC<{citation: Citation, i: number}> = ({citation, i}) => {
|
||||
|
||||
var c_str = citation.title;
|
||||
|
||||
if (citation.authors && citation.authors.length > 0)
|
||||
c_str += " - " + citation.authors.join(', ');
|
||||
if (citation.date && citation.date !== "")
|
||||
c_str += " - " + citation.date;
|
||||
|
||||
// if we don't have a url, link to a duckduckgo search for the title instead
|
||||
const url = citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
|
||||
return (
|
||||
<A className={Colours[i % Colours.length] + " border-2 flex items-center rounded my-2 text-sm no-underline w-fit"}
|
||||
href={url}>
|
||||
<span className="mx-1"> [{i + 1}] </span>
|
||||
<p className="mx-1 my-0"> {c_str} </p>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
const ShowInTextCitation: React.FC<{citation: Citation, i: number}> = ({citation, i}) => {
|
||||
const url = citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
return (
|
||||
<A className={Colours[i % Colours.length] + " border-2 rounded text-sm no-underline w-min px-0.5 pb-0.5 ml-1 mr-0.5"}
|
||||
href={url}>
|
||||
[{i + 1}]
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
const A: React.FC<{href: string, className?: string, children: React.ReactNode}> = ({href, className, children}) => {
|
||||
// link element that only populates the href field if the contents are there
|
||||
return href && href !== "" ? (
|
||||
<a className={className} href={href} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<a className={className}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// todo: memoize this if too slow.
|
||||
const ProcessText: (text: string, base_count: number) => [string, Map<string, number>] = (text, base_count) => {
|
||||
|
||||
// ---------------------- normalize citation form ----------------------
|
||||
// the general plan here is just to add parsing cases until we can respond
|
||||
// well to almost everything the LLM emits. We won't ever reach five nines,
|
||||
// but the domain is one where occasionally failing isn't catastrophic.
|
||||
|
||||
// transform all things that look like [a, b, c] into [a][b][c]
|
||||
let response = text.replace(
|
||||
|
||||
/\[((?:[a-z]+,\s*)*[a-z]+)\]/g, // identify groups of this form
|
||||
|
||||
(block: string) => block.split(',')
|
||||
.map((x) => x.trim())
|
||||
.join("][")
|
||||
)
|
||||
|
||||
// transform all things that look like [(a), (b), (c)] into [(a)][(b)][(c)]
|
||||
response = response.replace(
|
||||
|
||||
/\[((?:\([a-z]+\),\s*)*\([a-z]+\))\]/g, // identify groups of this form
|
||||
|
||||
(block: string) => block.split(',')
|
||||
.map((x) => x.trim())
|
||||
.join("][")
|
||||
)
|
||||
|
||||
// transform all things that look like [(a)] into [a]
|
||||
response = response.replace(
|
||||
/\[\(([a-z]+)\)\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
)
|
||||
|
||||
// transform all things that look like [ a ] into [a]
|
||||
response = response.replace(
|
||||
/\[\s*([a-z]+)\s*\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
)
|
||||
|
||||
// -------------- map citations from strings into numbers --------------
|
||||
|
||||
// figure out what citations are in the response, and map them appropriately
|
||||
const cite_map = new Map<string, number>();
|
||||
let cite_count = 0;
|
||||
|
||||
// scan a regex for [x] over the response. If x isn't in the map, add it.
|
||||
// (note: we're actually doing this twice - once on parsing, once on render.
|
||||
// if that looks like a problem, we could swap from strings to custom ropes).
|
||||
const regex = /\[([a-z]+)\]/g;
|
||||
let match;
|
||||
let response_copy = ""
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
if (!cite_map.has(match[1]!)) {
|
||||
cite_map.set(match[1]!, base_count + cite_count++);
|
||||
}
|
||||
// replace [x] with [i]
|
||||
response_copy += response.slice(response_copy.length, match.index) + `[${cite_map.get(match[1]!)! + 1}]`;
|
||||
}
|
||||
|
||||
response = response_copy + response.slice(response_copy.length);
|
||||
|
||||
return [response, cite_map]
|
||||
}
|
||||
|
||||
|
||||
const ShowAssistantEntry: React.FC<{entry: AssistantEntry}> = ({entry}) => {
|
||||
const in_text_citation_regex = /\[([0-9]+)\]/g;
|
||||
|
||||
let [response, cite_map] = ProcessText(entry.content, entry.base_count);
|
||||
|
||||
// ----------------- create the ordered citation array -----------------
|
||||
|
||||
const citations = new Map<number, Citation>();
|
||||
cite_map.forEach((value, key) => {
|
||||
const index = key.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
if (index >= entry.citations.length) {
|
||||
console.log("invalid citation index: " + index);
|
||||
} else {
|
||||
citations.set(value, entry.citations[index]!);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-3 mb-8">
|
||||
{ // split into paragraphs
|
||||
response.split("\n").map(paragraph => ( <p> {
|
||||
paragraph.split(in_text_citation_regex).map((text, i) => {
|
||||
if (i % 2 === 0) {
|
||||
return <GlossarySpan content={text.trim()} />;
|
||||
}
|
||||
i = parseInt(text) - 1;
|
||||
if (!citations.has(i)) return `[${text}]`;
|
||||
const citation = citations.get(i)!;
|
||||
return (
|
||||
<ShowInTextCitation citation={citation} i={i} />
|
||||
);
|
||||
})
|
||||
} </p>))
|
||||
}
|
||||
<ul className="mt-5">
|
||||
{ // show citations
|
||||
Array.from(citations.entries()).map(([i, citation]) => (
|
||||
<li key={i}>
|
||||
<ShowCitation citation={citation} i={i} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type State = {
|
||||
state: "idle";
|
||||
} | {
|
||||
@@ -499,34 +282,7 @@ const Home: NextPage = () => {
|
||||
</Head>
|
||||
<main>
|
||||
<Header page="index" />
|
||||
{/* three buttons for the three modes, place far right, 1rem between each */}
|
||||
<div className="flex flex-row justify-center w-fit ml-auto mr-0 mb-5 gap-2">
|
||||
<button className={
|
||||
"border border-gray-300 px-1 " + (mode[1] && mode[0] === "rookie" ? "bg-gray-200" : "")
|
||||
} onClick={() => { setMode(["rookie", true]); }}
|
||||
title="For people who are new to the field of AI alignment. The
|
||||
answer might be longer, since technical terms will be
|
||||
explained in more detail and less background will be
|
||||
assumed.">
|
||||
rookie
|
||||
</button>
|
||||
//
|
||||
<button className={
|
||||
"border border-gray-300 px-1 " + (mode[1] && mode[0] === "concise" ? "bg-gray-200" : "")
|
||||
} onClick={() => { setMode(["concise", true]); }}
|
||||
title="Quick and to the point. Followup questions may need to be
|
||||
asked to get the full picture of what's going on.">
|
||||
concise
|
||||
</button>
|
||||
//
|
||||
<button className={
|
||||
"border border-gray-300 px-1 " + (mode[1] && mode[0] === "default" ? "bg-gray-200" : "")
|
||||
} onClick={() => { setMode(["default", true]); }}
|
||||
title="A balanced default mode.">
|
||||
default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Controls mode={mode} setMode={setMode} />
|
||||
|
||||
<h2 className="bg-red-100 text-red-800"><b>WARNING</b>: This is a very <b>early prototype</b>. <Link href="http://bit.ly/stampy-chat-issues" target="_blank">Feedback</Link> welcomed.</h2>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type NextPage } from "next";
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
import Header from "../header";
|
||||
import { SearchBox, Followup } from "../searchbox";
|
||||
import Header from "../components/header";
|
||||
import { SearchBox, Followup } from "../components/searchbox";
|
||||
import { useState } from "react";
|
||||
import { API_URL } from "../settings"
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export type Citation = {
|
||||
title: string;
|
||||
authors: string[];
|
||||
date: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type Followup = {
|
||||
text: string;
|
||||
pageid: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage;
|
||||
|
||||
export type UserEntry = {
|
||||
role: "user";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type AssistantEntry = {
|
||||
role: "assistant";
|
||||
content: string;
|
||||
citations: Citation[];
|
||||
base_count: number; // the number to start counting citations at
|
||||
}
|
||||
|
||||
export type ErrorMessage = {
|
||||
role: "error";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type StampyMessage = {
|
||||
role: "stampy";
|
||||
content: string;
|
||||
url: string;
|
||||
}
|
||||
Reference in New Issue
Block a user