Restructure web components

This commit is contained in:
Daniel O'Connell
2023-09-28 22:45:51 +02:00
parent 132318ae63
commit ac7fdbc442
11 changed files with 292 additions and 263 deletions
+50
View File
@@ -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>
);
};
+103
View File
@@ -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>
);
};
+57
View File
@@ -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" ? (
+31
View File
@@ -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,
});
+1 -1
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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"
+37
View File
@@ -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;
}