mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add artifact diff viewer and improve button styling
- Add clickable artifact cards with keyboard support - Create new ArtifactView for displaying file diffs - Update button styles with solid backgrounds and faster transitions - Add auto-cleanup of stale diff and plan approval views on startup - Fix artifact deletion to preserve original file path
This commit is contained in:
parent
6b5d51df39
commit
82dab77d74
15 changed files with 422 additions and 146 deletions
|
|
@ -7,11 +7,14 @@
|
|||
import { fade } from "svelte/transition";
|
||||
import { ArtifactAction, artifactActionToCopy } from "Enums/ArtifactAction";
|
||||
import { basename } from "path-browserify";
|
||||
import type { Artifact } from "Conversations/Artifact";
|
||||
import type { DiffService } from "Services/DiffService";
|
||||
|
||||
export let message: ConversationContent;
|
||||
export let isSubmitting: boolean = false;
|
||||
|
||||
let streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
|
||||
let diffService: DiffService = Resolve<DiffService>(Services.DiffService);
|
||||
|
||||
function messageRenderAction(element: HTMLElement, message: ConversationContent) {
|
||||
streamingMarkdownService.render(message.getDisplayContent(), element);
|
||||
|
|
@ -47,6 +50,10 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleArtifactCardClick(artifact: Artifact) {
|
||||
diffService.showArtifactDiff(artifact);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="message-container assistant">
|
||||
|
|
@ -74,11 +81,15 @@
|
|||
<div
|
||||
class="artifact-card"
|
||||
aria-label="{artifact.filePath}"
|
||||
on:click={() => handleArtifactCardClick(artifact)}
|
||||
on:keydown={(e) => e.key === 'Enter' && handleArtifactCardClick(artifact)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="artifact-ellipse artifact-ellipse-{artifact.action}"></span>
|
||||
<div
|
||||
class="artifact-icon"
|
||||
use:setElementIcon={artifact.getIconName()}
|
||||
class="artifact-icon"
|
||||
use:setElementIcon={artifact.getIconName()}
|
||||
></div>
|
||||
<span class="artifact-name">{basename(artifact.filePath)}</span>
|
||||
<span class="artifact-action artifact-action-{artifact.action}">{artifactActionToCopy(artifact.action)}</span>
|
||||
|
|
@ -253,7 +264,8 @@
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
.artifact-card:hover .artifact-ellipse {
|
||||
.artifact-card:hover .artifact-ellipse,
|
||||
.artifact-card:focus-visible .artifact-ellipse {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-shadow: 0px 0px 4px 1px currentColor;
|
||||
|
|
@ -302,7 +314,7 @@
|
|||
justify-content: center;
|
||||
font-size: var(--font-smallest);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--size-4-2);
|
||||
border-radius: var(--size-4-1);
|
||||
padding: var(--size-2-1) var(--size-4-2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -862,7 +862,7 @@
|
|||
padding-left: var(--size-4-2);
|
||||
padding-right: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
transition: background-color 0.2s ease-out;
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,68 +45,45 @@
|
|||
|
||||
<style>
|
||||
#diff-controls-wrapper {
|
||||
transition: height 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
transition: height 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#diff-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--size-4-2) 1fr;
|
||||
grid-template-rows: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--size-4-2) 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
#diff-accept {
|
||||
grid-column: 1;
|
||||
color: var(--color-green);
|
||||
border: solid;
|
||||
border-color: var(--color-green);
|
||||
border-width: var(--size-2-1);
|
||||
background-color: var(--background-primary);
|
||||
grid-column: 1;
|
||||
color: white;
|
||||
background-color: #38533a;
|
||||
}
|
||||
|
||||
#diff-accept:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
#diff-accept:focus {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
#diff-reject {
|
||||
grid-column: 3;
|
||||
color: var(--color-red);
|
||||
border: solid;
|
||||
border-color: var(--color-red);
|
||||
border-width: var(--size-2-1);
|
||||
background-color: var(--background-primary);
|
||||
grid-column: 3;
|
||||
color: white;
|
||||
background-color: #593030;
|
||||
}
|
||||
|
||||
#diff-reject:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
#diff-reject:focus {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
.diff-button {
|
||||
border-radius: var(--button-radius);
|
||||
transition-duration: 0.5s;
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -46,68 +46,45 @@
|
|||
|
||||
<style>
|
||||
#plan-approval-controls-wrapper {
|
||||
transition: height 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
transition: height 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#plan-approval-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--size-4-2) 1fr;
|
||||
grid-template-rows: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--size-4-2) 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
#plan-approve {
|
||||
grid-column: 1;
|
||||
color: var(--color-green);
|
||||
border: solid;
|
||||
border-color: var(--color-green);
|
||||
border-width: var(--size-2-1);
|
||||
background-color: var(--background-primary);
|
||||
grid-column: 1;
|
||||
color: white;
|
||||
background-color: #38533a;
|
||||
}
|
||||
|
||||
#plan-approve:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
#plan-approve:focus {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
#plan-reject {
|
||||
grid-column: 3;
|
||||
color: var(--color-red);
|
||||
border: solid;
|
||||
border-color: var(--color-red);
|
||||
border-width: var(--size-2-1);
|
||||
background-color: var(--background-primary);
|
||||
grid-column: 3;
|
||||
color: white;
|
||||
background-color: #593030;
|
||||
}
|
||||
|
||||
#plan-reject:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
#plan-reject:focus {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
.plan-approval-button {
|
||||
border-radius: var(--button-radius);
|
||||
transition-duration: 0.5s;
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -143,14 +143,15 @@ export enum Copy {
|
|||
ButtonWebSearchUnavailable = "Web search unavailable",
|
||||
ButtonUserInstruction = "User Instruction",
|
||||
ButtonAttachFiles = "Attach Files",
|
||||
|
||||
// Plan Approval View
|
||||
PlanApprovalViewTitle = "Vaultkeeper AI plan",
|
||||
|
||||
ButtonApprove = "Approve",
|
||||
ButtonReject = "Reject",
|
||||
ButtonDiscuss = "Discuss",
|
||||
|
||||
ButtonRestore = "Restore this version",
|
||||
ButtonRestorePrevious = "Restore previous version",
|
||||
ButtonConfirm = "Confirm?",
|
||||
|
||||
// Agent file message
|
||||
AttachedFile = `The file {fileName} is attached and its full contents follow below. This is the actual content of the file — read it directly to answer the user. This attachment may be a file the user uploaded to the chat, or a vault file you retrieved with a tool; either way, the content below is authoritative and you do NOT need to read or fetch this file again.`,
|
||||
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ export class AIToolService {
|
|||
if (isBinaryFile(fileType) || isDocumentMimeType(FileTypeToMimeType[fileType])) {
|
||||
const result = await this.fileSystemService.readBinaryFile(filePath);
|
||||
if (result instanceof ArrayBuffer) {
|
||||
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], ArtifactAction.Delete, "", "", arrayBufferToBase64(result)));
|
||||
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], ArtifactAction.Delete, filePath, "", arrayBufferToBase64(result)));
|
||||
}
|
||||
} else {
|
||||
const result = await this.fileSystemService.readFilePath(filePath);
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ export class ConversationFileSystemService {
|
|||
}
|
||||
}
|
||||
|
||||
file.setStoragePath(filePath.replace(`${Path.Conversations}/`, ''));
|
||||
file.setStoragePath(filePath.replace(`${Path.Conversations}/`, ""));
|
||||
}
|
||||
|
||||
private async loadBinaryFile(storagePath: string): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { Diff2HtmlUIConfig } from 'diff2html/lib/ui/js/diff2html-ui-base';
|
|||
import { ColorSchemeType, OutputFormatType } from 'diff2html/lib/types';
|
||||
import { Component } from 'obsidian';
|
||||
import { AbortService } from './AbortService';
|
||||
import type { Artifact } from 'Conversations/Artifact';
|
||||
|
||||
interface DiffResult {
|
||||
accepted: boolean;
|
||||
|
|
@ -35,6 +36,26 @@ export class DiffService extends Component {
|
|||
}));
|
||||
}
|
||||
|
||||
public showArtifactDiff(artifact: Artifact): void {
|
||||
const diffString = this.createDiffString(artifact.filePath,
|
||||
artifact.filePath, artifact.originalContent, artifact.updatedContent);
|
||||
|
||||
const outputFormat: OutputFormatType = "line-by-line";
|
||||
|
||||
const config: Diff2HtmlUIConfig = {
|
||||
drawFileList: false,
|
||||
matching: "words",
|
||||
outputFormat: outputFormat,
|
||||
highlight: false,
|
||||
fileListToggle: false,
|
||||
fileContentToggle: false,
|
||||
synchronisedScroll: true,
|
||||
colorScheme: ColorSchemeType.AUTO
|
||||
};
|
||||
|
||||
void this.plugin.activateArtifactView(artifact, diffString, config);
|
||||
}
|
||||
|
||||
public async requestDiff(oldFileName: string, newFileName: string, oldContent: string, newContent: string): Promise<DiffResult> {
|
||||
const diffString = this.createDiffString(oldFileName, newFileName, oldContent, newContent);
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,16 @@ body:not(.is-tablet) .workspace-drawer.mod-right {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Artifact View Customization */
|
||||
/* ============================== */
|
||||
|
||||
.workspace-leaf-content[data-type="vaultkeeper-ai-artifact-view"] .view-content {
|
||||
container-type: size;
|
||||
container-name: artifact-container;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Settings Styles */
|
||||
/* ============================== */
|
||||
|
|
|
|||
|
|
@ -131,51 +131,35 @@
|
|||
.diff-mobile-controls .diff-mobile-button {
|
||||
min-height: 44px;
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--button-radius);
|
||||
cursor: pointer;
|
||||
border-width: var(--size-2-1);
|
||||
border-style: solid;
|
||||
background-color: var(--background-primary);
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-accept {
|
||||
color: var(--color-green);
|
||||
border-color: var(--color-green);
|
||||
color: white;
|
||||
background-color: #38533a;
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-accept:hover,
|
||||
.diff-mobile-controls .diff-mobile-accept:focus,
|
||||
.diff-mobile-controls .diff-mobile-accept:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-discuss {
|
||||
color: var(--interactive-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-discuss:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--interactive-accent) 25%,
|
||||
white 10%
|
||||
);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-reject {
|
||||
color: var(--color-red);
|
||||
border-color: var(--color-red);
|
||||
color: white;
|
||||
background-color: #593030;
|
||||
}
|
||||
|
||||
.diff-mobile-controls .diff-mobile-reject:hover,
|
||||
.diff-mobile-controls .diff-mobile-reject:focus,
|
||||
.diff-mobile-controls .diff-mobile-reject:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
/* Adjust diff height on mobile to account for buttons */
|
||||
|
|
@ -188,4 +172,74 @@
|
|||
.diff-view-mobile .d2h-file-diff {
|
||||
max-height: calc(100cqh - 37px - 120px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Artifact View Controls */
|
||||
/* (shown on desktop AND mobile) */
|
||||
/* ============================== */
|
||||
|
||||
.artifact-view-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--size-4-2);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--size-4-3);
|
||||
z-index: 10;
|
||||
background-color: var(--background-primary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-button {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
/* Avoid the floating obsidian controls on mobile */
|
||||
@media (max-width: 600px) {
|
||||
.artifact-view-controls {
|
||||
margin-bottom: 85px;
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-button {
|
||||
min-height: 44px;
|
||||
font-size: var(--font-ui-medium);
|
||||
border-radius: var(--button-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-button-confirming {
|
||||
min-width: var(--artifact-view-button-width);
|
||||
color: white;
|
||||
background-color: #38533a;
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-button-confirming:hover,
|
||||
.artifact-view-controls .artifact-view-button-confirming:focus-visible,
|
||||
.artifact-view-controls .artifact-view-button-confirming:active {
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
.artifact-view-controls .artifact-view-close {
|
||||
color: var(--interactive-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.workspace-leaf-content[data-type="vaultkeeper-ai-artifact-view"] .d2h-file-diff {
|
||||
max-height: calc(100cqh - 37px - 40px);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.workspace-leaf-content[data-type="vaultkeeper-ai-artifact-view"] .d2h-file-diff {
|
||||
max-height: calc(100cqh - 37px - 120px);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,51 +40,35 @@
|
|||
.plan-approval-mobile-controls .plan-approval-mobile-button {
|
||||
min-height: 44px;
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--button-radius);
|
||||
cursor: pointer;
|
||||
border-width: var(--size-2-1);
|
||||
border-style: solid;
|
||||
background-color: var(--background-primary);
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve {
|
||||
color: var(--color-green);
|
||||
border-color: var(--color-green);
|
||||
color: white;
|
||||
background-color: #38533a;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve:hover,
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve:focus,
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-approve:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #537555;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-discuss {
|
||||
color: var(--interactive-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-discuss:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--interactive-accent) 25%,
|
||||
white 10%
|
||||
);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject {
|
||||
color: var(--color-red);
|
||||
border-color: var(--color-red);
|
||||
color: white;
|
||||
background-color: #593030;
|
||||
}
|
||||
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject:hover,
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject:focus,
|
||||
.plan-approval-mobile-controls .plan-approval-mobile-reject:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
white 10%
|
||||
);
|
||||
background-color: #774545;
|
||||
}
|
||||
|
||||
/* Adjust plan height on mobile to account for buttons */
|
||||
|
|
|
|||
209
Views/ArtifactView.ts
Normal file
209
Views/ArtifactView.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { Diff2HtmlUI, type Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui-base";
|
||||
import { base64ToArrayBuffer, ItemView, WorkspaceLeaf, type ViewStateResult } from "obsidian";
|
||||
import type { Artifact } from "Conversations/Artifact";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { ArtifactAction } from "Enums/ArtifactAction";
|
||||
import type { FileSystemService } from "Services/FileSystemService";
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import type { WorkSpaceService } from "Services/WorkSpaceService";
|
||||
import { isDocumentMimeType, toMimeType } from "Enums/MimeType";
|
||||
|
||||
export const VIEW_TYPE_ARTIFACT = 'vaultkeeper-ai-artifact-view';
|
||||
|
||||
interface ArtifactViewState {
|
||||
artifact: Artifact;
|
||||
diffString: string;
|
||||
config: Diff2HtmlUIConfig;
|
||||
}
|
||||
|
||||
const CONFIRM_TIMEOUT_MS = 3000;
|
||||
|
||||
export class ArtifactView extends ItemView {
|
||||
|
||||
private readonly fileSystemService: FileSystemService;
|
||||
private readonly workSpaceService: WorkSpaceService;
|
||||
|
||||
private artifact: Artifact | undefined;
|
||||
private diffString: string = "";
|
||||
private config: Diff2HtmlUIConfig = {};
|
||||
|
||||
private diffContainer: HTMLElement | null = null;
|
||||
private buttonsContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
||||
this.workSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
||||
}
|
||||
|
||||
public getViewType(): string {
|
||||
return VIEW_TYPE_ARTIFACT;
|
||||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return "Vaultkeeper AI artifact viewer";
|
||||
}
|
||||
|
||||
public async setState(state: ArtifactViewState, result: ViewStateResult): Promise<void> {
|
||||
this.artifact = state.artifact;
|
||||
this.diffString = state.diffString;
|
||||
this.config = state.config;
|
||||
|
||||
this.renderDiff();
|
||||
|
||||
return super.setState(state, result);
|
||||
}
|
||||
|
||||
public getState(): Record<string, unknown> {
|
||||
return {
|
||||
artifact: this.artifact,
|
||||
diffString: this.diffString,
|
||||
config: this.config
|
||||
};
|
||||
}
|
||||
|
||||
private renderDiff() {
|
||||
if (!this.artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.resetContainer();
|
||||
|
||||
this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' });
|
||||
const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config);
|
||||
|
||||
diff2htmlUi.draw();
|
||||
|
||||
const buttonsContainer = this.createButtons();
|
||||
if (buttonsContainer) {
|
||||
this.buttonsContainer = buttonsContainer;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const firstChange = this.diffContainer?.querySelector('.d2h-change, .d2h-ins, .d2h-del');
|
||||
firstChange?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
}
|
||||
|
||||
private resetContainer(): HTMLElement {
|
||||
const container = this.contentEl;
|
||||
container.empty();
|
||||
|
||||
if (this.diffContainer) {
|
||||
this.diffContainer.remove();
|
||||
this.diffContainer = null;
|
||||
}
|
||||
|
||||
if (this.buttonsContainer) {
|
||||
this.buttonsContainer.remove();
|
||||
this.buttonsContainer = null;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createButtons(): HTMLElement | undefined {
|
||||
if (!this.artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.contentEl.createDiv({ cls: 'artifact-view-controls' });
|
||||
|
||||
// If a file was modified then we have the option of restoring original content or the updated content
|
||||
// If the operation was create / delete then we only restore updated or original respectively
|
||||
// Binary / office file formats cannot be modified or created so we only ever restore the original for these
|
||||
|
||||
// We only need the restore previous button if the action was a modify
|
||||
if (this.artifact.action === ArtifactAction.Modify) {
|
||||
const restorePreviousButton = container.createEl('button', {
|
||||
cls: 'artifact-view-button',
|
||||
text: Copy.ButtonRestorePrevious
|
||||
});
|
||||
|
||||
this.registerConfirmButton(restorePreviousButton, Copy.ButtonRestorePrevious, async () => {
|
||||
if (!this.artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fileSystemService.writeToFilePath(this.artifact.filePath, this.artifact.originalContent, false, false);
|
||||
await this.closeArtifactView();
|
||||
});
|
||||
}
|
||||
|
||||
const restoreButton = container.createEl('button', {
|
||||
cls: 'artifact-view-button',
|
||||
text: Copy.ButtonRestore
|
||||
});
|
||||
|
||||
this.registerConfirmButton(restoreButton, Copy.ButtonRestore, async () => {
|
||||
if (!this.artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.artifact.base64) {
|
||||
const arrayBuffer = base64ToArrayBuffer(this.artifact.base64);
|
||||
await this.fileSystemService.writeBinaryFile(this.artifact.filePath, arrayBuffer, false);
|
||||
await this.closeArtifactView();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the action is delete then we restore original otherwise restore updated
|
||||
const restoreOriginal = this.artifact.action === ArtifactAction.Delete;
|
||||
|
||||
await this.fileSystemService.writeToFilePath(this.artifact.filePath, restoreOriginal
|
||||
? this.artifact.originalContent : this.artifact.updatedContent, false, false);
|
||||
|
||||
await this.closeArtifactView();
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private registerConfirmButton(button: HTMLButtonElement, defaultLabel: string, onConfirm: () => Promise<void>): void {
|
||||
button.setAttribute('aria-label', defaultLabel);
|
||||
|
||||
let resetTimeoutId: number | undefined;
|
||||
let awaitingConfirmation = false;
|
||||
|
||||
const reset = () => {
|
||||
awaitingConfirmation = false;
|
||||
button.setText(defaultLabel);
|
||||
button.setAttribute('aria-label', defaultLabel);
|
||||
button.removeClass('artifact-view-button-confirming');
|
||||
if (resetTimeoutId !== undefined) {
|
||||
window.clearTimeout(resetTimeoutId);
|
||||
resetTimeoutId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
this.registerDomEvent(button, 'click', async () => {
|
||||
if (!awaitingConfirmation) {
|
||||
awaitingConfirmation = true;
|
||||
button.setCssProps({ '--artifact-view-button-width': `${button.offsetWidth}px` });
|
||||
button.addClass('artifact-view-button-confirming');
|
||||
button.setText(Copy.ButtonConfirm);
|
||||
button.setAttribute('aria-label', Copy.ButtonConfirm);
|
||||
button.blur();
|
||||
resetTimeoutId = window.setTimeout(reset, CONFIRM_TIMEOUT_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
reset();
|
||||
await onConfirm();
|
||||
});
|
||||
|
||||
this.register(reset);
|
||||
}
|
||||
|
||||
private async closeArtifactView(): Promise<void> {
|
||||
if (!this.artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDocumentMimeType(toMimeType(this.artifact.mimeType))) {
|
||||
await this.workSpaceService.openNoteByPath(this.artifact.filePath);
|
||||
}
|
||||
this.leaf.detach();
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export class DiffView extends ItemView {
|
|||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return "Vaultkeeper AI diff";
|
||||
return "Vaultkeeper AI diff viewer";
|
||||
}
|
||||
|
||||
public async setState(state: DiffViewState, result: ViewStateResult): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class PlanApprovalView extends ItemView {
|
|||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return Copy.PlanApprovalViewTitle;
|
||||
return "Vaultkeeper AI plan";
|
||||
}
|
||||
|
||||
public async setState(state: PlanApprovalViewState, result: ViewStateResult): Promise<void> {
|
||||
|
|
|
|||
31
main.ts
31
main.ts
|
|
@ -17,6 +17,8 @@ import "katex/dist/katex.min.css";
|
|||
import 'highlight.js/styles/monokai.min.css';
|
||||
import 'diff2html/bundles/css/diff2html.min.css';
|
||||
import type { AssetsService } from "Services/AssetsService";
|
||||
import type { Artifact } from "Conversations/Artifact";
|
||||
import { ArtifactView, VIEW_TYPE_ARTIFACT } from "Views/ArtifactView";
|
||||
|
||||
export default class VaultkeeperAIPlugin extends Plugin {
|
||||
|
||||
|
|
@ -32,6 +34,10 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
VIEW_TYPE_DIFF,
|
||||
(leaf) => new DiffView(leaf)
|
||||
);
|
||||
this.registerView(
|
||||
VIEW_TYPE_ARTIFACT,
|
||||
(leaf) => new ArtifactView(leaf)
|
||||
);
|
||||
this.registerView(
|
||||
VIEW_TYPE_PLAN_APPROVAL,
|
||||
(leaf) => new PlanApprovalView(leaf)
|
||||
|
|
@ -53,6 +59,7 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
this.addSettingTab(new VaultkeeperAISettingTab());
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
this.closeStaleViews();
|
||||
await this.setup();
|
||||
});
|
||||
}
|
||||
|
|
@ -113,6 +120,30 @@ export default class VaultkeeperAIPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
public async activateArtifactView(artifact: Artifact, diffString: string, config: Diff2HtmlUIConfig) {
|
||||
const { workspace } = this.app;
|
||||
|
||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_ARTIFACT);
|
||||
const leaf = leaves.length > 0 ? leaves[0] : workspace.getLeaf("tab");
|
||||
|
||||
await leaf?.setViewState({
|
||||
type: VIEW_TYPE_ARTIFACT,
|
||||
active: true,
|
||||
state: { artifact, diffString, config }
|
||||
});
|
||||
|
||||
if (leaf != null) {
|
||||
await workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
private closeStaleViews() {
|
||||
const { workspace } = this.app;
|
||||
|
||||
workspace.getLeavesOfType(VIEW_TYPE_DIFF).forEach(leaf => leaf.detach());
|
||||
workspace.getLeavesOfType(VIEW_TYPE_PLAN_APPROVAL).forEach(leaf => leaf.detach());
|
||||
}
|
||||
|
||||
// create example user instruction (on first launch only)
|
||||
private async setup() {
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
|
|
|
|||
Loading…
Reference in a new issue