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,
SPREADSHEET_FORMATS,
TEXT_EDITORS,
TEXT_FILE_EXTENSIONS,
VIDEO_FILE_EXTENSIONS,
} from "utils/constants";
import {
@ -52,6 +53,12 @@ import {
} from "utils/imagemagick/formats";
import { type ImageMagickConvertFile } from "utils/imagemagick/types";
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;
@ -92,6 +99,7 @@ const useFileContextMenu = (
updateFolder,
} = useFileSystem();
const { contextMenu } = useMenu();
const hasWindowAI = useWindowAI();
const { onContextMenuCapture, ...contextMenuHandlers } = useMemo(
() =>
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 (
hasBackgroundVideoExtension ||
(IMAGE_FILE_EXTENSIONS.has(pathExtension) &&
@ -610,6 +647,7 @@ const useFileContextMenu = (
extractFiles,
fileManagerId,
focusedEntries,
hasWindowAI,
isFocusedEntry,
lstat,
mapFs,

View File

@ -1,3 +1,4 @@
import { extname } from "path";
import { useTheme } from "styled-components";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
@ -18,7 +19,7 @@ import {
} from "components/system/Taskbar/AI/icons";
import useAITransition from "components/system/Taskbar/AI/useAITransition";
import {
AI_STAGE,
AI_DISPLAY_TITLE,
AI_TITLE,
AI_WORKER,
DEFAULT_CONVO_STYLE,
@ -41,6 +42,7 @@ import useWorker from "hooks/useWorker";
import useFocusable from "components/system/Window/useFocusable";
import { useSession } from "contexts/session";
import { useWindowAI } from "hooks/useWindowAI";
import { useFileSystem } from "contexts/fileSystem";
type AIChatProps = {
toggleAI: () => void;
@ -62,7 +64,7 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
const [convoStyle, setConvoStyle] = useState(DEFAULT_CONVO_STYLE);
const [primaryColor, secondaryColor, tertiaryColor] =
taskbarColor.ai[convoStyle];
const [promptText, setPromptText] = useState("");
const [promptText, setPromptText] = useState(window.initialAiPrompt || "");
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const sectionRef = useRef<HTMLDivElement>(null);
const typing = promptText.length > 0;
@ -155,6 +157,45 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
textArea.style.height = "auto";
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(() => {
textAreaRef.current?.focus(PREVENT_SCROLL);
@ -192,21 +233,16 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
conversation.length > 0 &&
conversation[conversation.length - 1].type === "user"
) {
const { text } = conversation[conversation.length - 1];
setResponding(true);
sessionIdRef.current ||= Date.now();
aiWorker.current.postMessage({
hasWindowAI,
id: sessionIdRef.current,
streamId: STREAMING_SUPPORT ? conversation.length : undefined,
style: convoStyle,
text,
});
sendMessage();
}
}, [aiWorker, conversation, convoStyle, hasWindowAI]);
}, [aiWorker, conversation, sendMessage]);
useEffect(() => {
if (window.initialAiPrompt && aiWorker.current) {
window.initialAiPrompt = "";
addUserPrompt();
}
}, [addUserPrompt, aiWorker]);
useEffect(() => {
const workerRef = aiWorker.current;
@ -259,7 +295,7 @@ const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
>
<div className="header">
<header>
{`${AI_TITLE} (${AI_STAGE})`}
{AI_DISPLAY_TITLE}
<nav>
<Button
className="close"

View File

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

View File

@ -4,11 +4,17 @@ import { type MarkedOptions } from "components/apps/Marked/useMarked";
declare global {
/* eslint-disable vars-on-top, no-var */
var ai: { languageModel: AILanguageModelFactory };
var ai: {
languageModel: AILanguageModelFactory;
summarizer: AISummarizerFactory;
};
var marked: {
parse: (markdownString: string, options: MarkedOptions) => string;
};
/* eslint-enable vars-on-top, no-var */
interface Window {
initialAiPrompt?: string;
}
}
export type MessageTypes = "user" | "ai";
@ -26,6 +32,7 @@ export type WorkerMessage = {
id: number;
streamId?: number;
style: ConvoStyles;
summarizeText?: 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 TEXT_FILE_EXTENSIONS = new Set([
".html",
".htm",
".whtml",
".md",
".txt",
]);
export const EDITABLE_IMAGE_FILE_EXTENSIONS = new Set([
".bmp",
".gif",