Doc summary via AI
Some checks are pending
Tests / tests (push) Waiting to run

This commit is contained in:
Dustin Brett 2024-10-20 21:48:16 -07:00
parent 53c1e1bfbf
commit f941b6a65d
5 changed files with 156 additions and 24 deletions

View File

@ -36,6 +36,7 @@ import {
SHORTCUT_EXTENSION, SHORTCUT_EXTENSION,
SPREADSHEET_FORMATS, SPREADSHEET_FORMATS,
TEXT_EDITORS, TEXT_EDITORS,
TEXT_FILE_EXTENSIONS,
VIDEO_FILE_EXTENSIONS, VIDEO_FILE_EXTENSIONS,
} from "utils/constants"; } from "utils/constants";
import { import {
@ -52,6 +53,12 @@ import {
} from "utils/imagemagick/formats"; } from "utils/imagemagick/formats";
import { type ImageMagickConvertFile } from "utils/imagemagick/types"; import { type ImageMagickConvertFile } from "utils/imagemagick/types";
import { Share } from "components/system/Menu/MenuIcons"; import { Share } from "components/system/Menu/MenuIcons";
import { useWindowAI } from "hooks/useWindowAI";
import { getNavButtonByTitle } from "hooks/useGlobalKeyboardShortcuts";
import {
AI_DISPLAY_TITLE,
AI_STAGE,
} from "components/system/Taskbar/AI/constants";
const { alias } = PACKAGE_DATA; const { alias } = PACKAGE_DATA;
@ -92,6 +99,7 @@ const useFileContextMenu = (
updateFolder, updateFolder,
} = useFileSystem(); } = useFileSystem();
const { contextMenu } = useMenu(); const { contextMenu } = useMenu();
const hasWindowAI = useWindowAI();
const { onContextMenuCapture, ...contextMenuHandlers } = useMemo( const { onContextMenuCapture, ...contextMenuHandlers } = useMemo(
() => () =>
contextMenu?.(() => { contextMenu?.(() => {
@ -485,6 +493,35 @@ const useFileContextMenu = (
}); });
} }
if (
hasWindowAI &&
"summarizer" in window.ai &&
TEXT_FILE_EXTENSIONS.has(urlExtension)
) {
const aiCommand = (command: string): void => {
const aiButton = getNavButtonByTitle(AI_DISPLAY_TITLE);
if (aiButton) {
window.initialAiPrompt = `${command}: ${url}`;
aiButton.click();
}
};
menuItems.unshift(MENU_SEPERATOR, {
label: `AI (${AI_STAGE})`,
menu: [
...("summarizer" in window.ai
? [
{
action: () => aiCommand("Summarize"),
label: "Summarize Text",
},
]
: []),
],
});
}
if ( if (
hasBackgroundVideoExtension || hasBackgroundVideoExtension ||
(IMAGE_FILE_EXTENSIONS.has(pathExtension) && (IMAGE_FILE_EXTENSIONS.has(pathExtension) &&
@ -610,6 +647,7 @@ const useFileContextMenu = (
extractFiles, extractFiles,
fileManagerId, fileManagerId,
focusedEntries, focusedEntries,
hasWindowAI,
isFocusedEntry, isFocusedEntry,
lstat, lstat,
mapFs, mapFs,

View File

@ -1,3 +1,4 @@
import { extname } from "path";
import { useTheme } from "styled-components"; import { useTheme } from "styled-components";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
@ -18,7 +19,7 @@ import {
} from "components/system/Taskbar/AI/icons"; } from "components/system/Taskbar/AI/icons";
import useAITransition from "components/system/Taskbar/AI/useAITransition"; import useAITransition from "components/system/Taskbar/AI/useAITransition";
import { import {
AI_STAGE, AI_DISPLAY_TITLE,
AI_TITLE, AI_TITLE,
AI_WORKER, AI_WORKER,
DEFAULT_CONVO_STYLE, DEFAULT_CONVO_STYLE,
@ -41,6 +42,7 @@ import useWorker from "hooks/useWorker";
import useFocusable from "components/system/Window/useFocusable"; import useFocusable from "components/system/Window/useFocusable";
import { useSession } from "contexts/session"; import { useSession } from "contexts/session";
import { useWindowAI } from "hooks/useWindowAI"; import { useWindowAI } from "hooks/useWindowAI";
import { useFileSystem } from "contexts/fileSystem";
type AIChatProps = { type AIChatProps = {
toggleAI: () => void; toggleAI: () => void;
@ -62,7 +64,7 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
const [convoStyle, setConvoStyle] = useState(DEFAULT_CONVO_STYLE); const [convoStyle, setConvoStyle] = useState(DEFAULT_CONVO_STYLE);
const [primaryColor, secondaryColor, tertiaryColor] = const [primaryColor, secondaryColor, tertiaryColor] =
taskbarColor.ai[convoStyle]; taskbarColor.ai[convoStyle];
const [promptText, setPromptText] = useState(""); const [promptText, setPromptText] = useState(window.initialAiPrompt || "");
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const sectionRef = useRef<HTMLDivElement>(null); const sectionRef = useRef<HTMLDivElement>(null);
const typing = promptText.length > 0; const typing = promptText.length > 0;
@ -155,6 +157,45 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
textArea.style.height = "auto"; textArea.style.height = "auto";
textArea.style.height = `${textArea.scrollHeight}px`; textArea.style.height = `${textArea.scrollHeight}px`;
}, []); }, []);
const { exists, readFile, stat } = useFileSystem();
const sendMessage = useCallback(async () => {
const { text } = conversation[conversation.length - 1];
setResponding(true);
sessionIdRef.current ||= Date.now();
let summarizeText = "";
const lcText = text.toLowerCase();
if (lcText.startsWith("summarize: /")) {
const docPath = text.slice(11).trim();
if ((await exists(docPath)) && !(await stat(docPath)).isDirectory()) {
let docText = (await readFile(docPath)).toString();
if ([".html", ".htm", ".whtml"].includes(extname(docPath))) {
const domContent = new DOMParser().parseFromString(
docText,
"text/html"
);
docText = domContent.body.textContent || "";
}
summarizeText = docText;
}
}
aiWorker.current?.postMessage({
hasWindowAI,
id: sessionIdRef.current,
streamId: STREAMING_SUPPORT ? conversation.length : undefined,
style: convoStyle,
summarizeText,
text,
});
}, [aiWorker, conversation, convoStyle, exists, hasWindowAI, readFile, stat]);
useEffect(() => { useEffect(() => {
textAreaRef.current?.focus(PREVENT_SCROLL); textAreaRef.current?.focus(PREVENT_SCROLL);
@ -192,21 +233,16 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
conversation.length > 0 && conversation.length > 0 &&
conversation[conversation.length - 1].type === "user" conversation[conversation.length - 1].type === "user"
) { ) {
const { text } = conversation[conversation.length - 1]; sendMessage();
setResponding(true);
sessionIdRef.current ||= Date.now();
aiWorker.current.postMessage({
hasWindowAI,
id: sessionIdRef.current,
streamId: STREAMING_SUPPORT ? conversation.length : undefined,
style: convoStyle,
text,
});
} }
}, [aiWorker, conversation, convoStyle, hasWindowAI]); }, [aiWorker, conversation, sendMessage]);
useEffect(() => {
if (window.initialAiPrompt && aiWorker.current) {
window.initialAiPrompt = "";
addUserPrompt();
}
}, [addUserPrompt, aiWorker]);
useEffect(() => { useEffect(() => {
const workerRef = aiWorker.current; const workerRef = aiWorker.current;
@ -259,7 +295,7 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
> >
<div className="header"> <div className="header">
<header> <header>
{`${AI_TITLE} (${AI_STAGE})`} {AI_DISPLAY_TITLE}
<nav> <nav>
<Button <Button
className="close" className="close"

View File

@ -49,6 +49,9 @@ let responding = false;
let sessionId = 0; let sessionId = 0;
let session: AILanguageModel | ChatCompletionMessageParam[] | undefined; let session: AILanguageModel | ChatCompletionMessageParam[] | undefined;
let summarizer: AISummarizer | undefined;
let prompts: (AILanguageModelAssistantPrompt | AILanguageModelUserPrompt)[] =
[];
let engine: MLCEngine; let engine: MLCEngine;
let markedLoaded = false; let markedLoaded = false;
@ -67,6 +70,8 @@ globalThis.addEventListener(
sessionId = data.id; sessionId = data.id;
if (data.hasWindowAI) { if (data.hasWindowAI) {
prompts = [];
summarizer?.destroy();
(session as AILanguageModel)?.destroy(); (session as AILanguageModel)?.destroy();
const config: AILanguageModelCreateOptionsWithSystemPrompt = { const config: AILanguageModelCreateOptionsWithSystemPrompt = {
@ -95,6 +100,16 @@ globalThis.addEventListener(
let retry = 0; let retry = 0;
try { try {
if (
data.hasWindowAI &&
data.summarizeText &&
"summarizer" in globalThis.ai &&
(await globalThis.ai.summarizer.capabilities())?.available ===
"readily"
) {
summarizer = await globalThis.ai.summarizer.create();
}
while (retry++ < 3 && !response) { while (retry++ < 3 && !response) {
if (cancel) break; if (cancel) break;
@ -102,10 +117,33 @@ globalThis.addEventListener(
if (data.hasWindowAI) { if (data.hasWindowAI) {
const aiAssistant = session as AILanguageModel; const aiAssistant = session as AILanguageModel;
response = data.streamId if (summarizer && data.summarizeText) {
? aiAssistant?.promptStreaming(data.text) // eslint-disable-next-line no-await-in-loop
: // eslint-disable-next-line no-await-in-loop response = await summarizer.summarize(data.summarizeText);
(await aiAssistant?.prompt(data.text)) || "";
(session as AILanguageModel)?.destroy();
prompts.push(
{ content: data.text, role: "user" },
{ content: response, role: "assistant" }
);
const config: AILanguageModelCreateOptionsWithSystemPrompt = {
...CONVO_STYLE_TEMPS[data.style],
initialPrompts: [
SYSTEM_PROMPT as unknown as AILanguageModelAssistantPrompt,
...prompts,
],
};
// eslint-disable-next-line no-await-in-loop
session = await globalThis.ai.languageModel.create(config);
} else if (aiAssistant) {
response = data.streamId
? aiAssistant.promptStreaming(data.text)
: // eslint-disable-next-line no-await-in-loop
(await aiAssistant.prompt(data.text)) || "";
}
} else { } else {
(session as ChatCompletionMessageParam[]).push({ (session as ChatCompletionMessageParam[]).push({
content: data.text, content: data.text,
@ -143,7 +181,7 @@ globalThis.addEventListener(
markedLoaded = true; markedLoaded = true;
} }
const sendMessage = (message: string, streamId?: number): void => const sendMessage = (message: string, streamId?: number): void => {
globalThis.postMessage({ globalThis.postMessage({
formattedResponse: globalThis.marked.parse(message, { formattedResponse: globalThis.marked.parse(message, {
headerIds: false, headerIds: false,
@ -152,8 +190,13 @@ globalThis.addEventListener(
response: message, response: message,
streamId, streamId,
}); });
prompts.push(
{ content: data.text, role: "user" },
{ content: message, role: "assistant" }
);
};
if (typeof response === "string") { if (response && typeof response === "string") {
sendMessage(response); sendMessage(response);
} else { } else {
try { try {

View File

@ -4,11 +4,17 @@ import { type MarkedOptions } from "components/apps/Marked/useMarked";
declare global { declare global {
/* eslint-disable vars-on-top, no-var */ /* eslint-disable vars-on-top, no-var */
var ai: { languageModel: AILanguageModelFactory }; var ai: {
languageModel: AILanguageModelFactory;
summarizer: AISummarizerFactory;
};
var marked: { var marked: {
parse: (markdownString: string, options: MarkedOptions) => string; parse: (markdownString: string, options: MarkedOptions) => string;
}; };
/* eslint-enable vars-on-top, no-var */ /* eslint-enable vars-on-top, no-var */
interface Window {
initialAiPrompt?: string;
}
} }
export type MessageTypes = "user" | "ai"; export type MessageTypes = "user" | "ai";
@ -26,6 +32,7 @@ export type WorkerMessage = {
id: number; id: number;
streamId?: number; streamId?: number;
style: ConvoStyles; style: ConvoStyles;
summarizeText?: string;
text: string; text: string;
}; };

View File

@ -133,6 +133,14 @@ export const TEXT_EDITORS = ["MonacoEditor", "Vim"];
export const CURSOR_FILE_EXTENSIONS = new Set([".ani", ".cur"]); export const CURSOR_FILE_EXTENSIONS = new Set([".ani", ".cur"]);
export const TEXT_FILE_EXTENSIONS = new Set([
".html",
".htm",
".whtml",
".md",
".txt",
]);
export const EDITABLE_IMAGE_FILE_EXTENSIONS = new Set([ export const EDITABLE_IMAGE_FILE_EXTENSIONS = new Set([
".bmp", ".bmp",
".gif", ".gif",