mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add file change summary cards to assistant messages
Display artifact changes in chat with visual indicators for create/modify/delete actions, including file counts, color-coded status badges, and scrollable file list
This commit is contained in:
parent
cc45949eba
commit
f4c3b5b826
10 changed files with 389 additions and 41 deletions
|
|
@ -12,6 +12,8 @@
|
|||
import { setIcon } from "obsidian";
|
||||
import { fade } from "svelte/transition";
|
||||
import GraphAnimation from "./GraphAnimation.svelte";
|
||||
import { ArtifactAction, artifactActionToCopy } from "Enums/ArtifactAction";
|
||||
import { basename } from "path-browserify";
|
||||
|
||||
export let messages: ConversationContent[] = [];
|
||||
export let currentThought: string | null = null;
|
||||
|
|
@ -109,6 +111,14 @@
|
|||
};
|
||||
}
|
||||
|
||||
function tallyArtifactsByAction(artifacts: { action: ArtifactAction }[]): Partial<Record<ArtifactAction, number>> {
|
||||
const tally: Partial<Record<ArtifactAction, number>> = {};
|
||||
for (const artifact of artifacts) {
|
||||
tally[artifact.action] = (tally[artifact.action] ?? 0) + 1;
|
||||
}
|
||||
return tally;
|
||||
}
|
||||
|
||||
$: if (scrollToBottomButton) {
|
||||
setIcon(scrollToBottomButton, "arrow-down");
|
||||
}
|
||||
|
|
@ -158,6 +168,35 @@
|
|||
<div class="markdown-content">
|
||||
<div use:messageRenderAction={message} class="streaming-content"></div>
|
||||
</div>
|
||||
{#if message.artifacts.length > 0}
|
||||
{@const artifactTally = tallyArtifactsByAction(message.artifacts)}
|
||||
<div class="artifacts-container" in:fade={{ duration: 300 }}>
|
||||
<span class="artifacts-container-title">{message.artifacts.length} FILES CHANGED</span>
|
||||
<div class="artifacts-tally">
|
||||
{#each Object.values(ArtifactAction) as action}
|
||||
{#if artifactTally[action]}
|
||||
<div class="artifact-tally-container">
|
||||
<span class="artifact-tally-ellipse artifact-{action}"></span>
|
||||
<span class="artifact-tally-count">{artifactTally[action]}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="artifacts-list-container">
|
||||
{#each message.artifacts as artifact}
|
||||
<div class="artifact-card" aria-label="{artifact.filePath}">
|
||||
<span class="artifact-ellipse artifact-{artifact.action}"></span>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -270,6 +309,7 @@
|
|||
}
|
||||
|
||||
.message-group.latest {
|
||||
margin-top: var(--size-4-2);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +356,7 @@
|
|||
|
||||
.message-bubble.assistant {
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-text-user-container {
|
||||
|
|
@ -333,7 +373,182 @@
|
|||
.message-text-user-container {
|
||||
padding-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
|
||||
.artifacts-container {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
width: 100%;
|
||||
margin-top: var(--size-4-3);
|
||||
gap: var(--size-4-4);
|
||||
}
|
||||
|
||||
.artifacts-container-title {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
font-size: var(--font-smallest);
|
||||
}
|
||||
|
||||
.artifacts-tally {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.artifact-tally-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.artifact-tally-ellipse {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: var(--size-4-2);
|
||||
}
|
||||
|
||||
.artifact-create {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
.artifact-modify {
|
||||
background: var(--color-blue);
|
||||
}
|
||||
|
||||
.artifact-delete {
|
||||
background: var(--color-red);
|
||||
}
|
||||
|
||||
.artifact-tally-count {
|
||||
font-size: var(--font-smallest);
|
||||
margin-left: var(--size-4-1);
|
||||
}
|
||||
|
||||
.artifacts-list-container {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: scroll;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
|
||||
.artifacts-list-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.artifact-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
gap: var(--size-4-3);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--background-secondary-alt);
|
||||
border-radius: var(--size-4-2);
|
||||
padding: 0 var(--size-4-1) 0 var(--size-4-2);
|
||||
}
|
||||
|
||||
.artifact-ellipse {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.artifact-ellipse-create {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
.artifact-ellipse-modify {
|
||||
background: var(--color-blue);
|
||||
}
|
||||
|
||||
.artifact-ellipse-delete {
|
||||
background: var(--color-red);
|
||||
}
|
||||
|
||||
.artifact-icon {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.artifact-name {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
font-size: var(--font-smaller);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.artifact-action {
|
||||
grid-row: 1;
|
||||
grid-column: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-smallest);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--size-4-2);
|
||||
padding: var(--size-2-1) var(--size-4-2);
|
||||
}
|
||||
|
||||
.artifact-action-create {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 25%,
|
||||
black 20%
|
||||
);
|
||||
color: color-mix(
|
||||
in srgb,
|
||||
var(--color-green) 100%,
|
||||
white 10%
|
||||
);
|
||||
}
|
||||
|
||||
.artifact-action-modify {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-blue) 25%,
|
||||
black 20%
|
||||
);
|
||||
color: color-mix(
|
||||
in srgb,
|
||||
var(--color-blue) 100%,
|
||||
white 10%
|
||||
);
|
||||
}
|
||||
|
||||
.artifact-action-delete {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 25%,
|
||||
black 20%
|
||||
);
|
||||
color: color-mix(
|
||||
in srgb,
|
||||
var(--color-red) 100%,
|
||||
white 10%
|
||||
);
|
||||
}
|
||||
|
||||
.conversation-empty-container {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { ARTIFACT_ACTION_RANK, ArtifactAction } from "Enums/ArtifactAction";
|
||||
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import ChatArea from "./ChatArea.svelte";
|
||||
|
|
@ -21,9 +23,10 @@
|
|||
import { AITool, fromString } from "Enums/AITool";
|
||||
import { AIProvider } from "Enums/ApiProvider";
|
||||
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
||||
import type { Artifact } from "Conversations/Artifact";
|
||||
import { Artifact } from "Conversations/Artifact";
|
||||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import { basename } from "path-browserify";
|
||||
|
||||
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
|
||||
|
|
@ -174,6 +177,7 @@
|
|||
},
|
||||
onComplete: async () => {
|
||||
saveCollectedArtifects(conversation);
|
||||
conversation = conversation;
|
||||
conversationService.saveConversation(conversation);
|
||||
isSubmitting = false;
|
||||
busyPlanning = false;
|
||||
|
|
@ -194,7 +198,7 @@
|
|||
conversation.contents.push(lastMessage);
|
||||
}
|
||||
|
||||
lastMessage.artifacts = collectedArtifacts;
|
||||
lastMessage.artifacts = Artifact.sort(collectedArtifacts);
|
||||
}
|
||||
|
||||
$: if ($conversationStore.shouldReset) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,31 @@
|
|||
import type { IBinaryFile } from "Conversations/IBinaryFile";
|
||||
import { ARTIFACT_ACTION_RANK, isArtifactAction, type ArtifactAction } from "Enums/ArtifactAction";
|
||||
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
|
||||
import { pathExtname } from "Helpers/Helpers";
|
||||
import { basename } from "path-browserify";
|
||||
|
||||
export class Artifact implements IBinaryFile {
|
||||
|
||||
public filePath: string;
|
||||
public mimeType: string;
|
||||
public action: ArtifactAction;
|
||||
public originalContent: string;
|
||||
public updatedContent: string;
|
||||
public base64: string | undefined;
|
||||
public artifactPath?: string;
|
||||
|
||||
public constructor(filePath: string, mimeType: string, originalContent: string, updatedContent: string, base64?: string, artifactPath?: string) {
|
||||
public static sort(artifacts: Artifact[]): Artifact[] {
|
||||
artifacts.sort((a, b) =>
|
||||
ARTIFACT_ACTION_RANK[a.action] - ARTIFACT_ACTION_RANK[b.action] ||
|
||||
basename(a.filePath).localeCompare(basename(b.filePath))
|
||||
);
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
public constructor(filePath: string, mimeType: string, action: ArtifactAction, originalContent: string, updatedContent: string, base64?: string, artifactPath?: string) {
|
||||
this.filePath = filePath;
|
||||
this.mimeType = mimeType;
|
||||
this.action = action;
|
||||
this.originalContent = originalContent;
|
||||
this.updatedContent = updatedContent;
|
||||
this.base64 = base64;
|
||||
|
|
@ -26,9 +40,31 @@ export class Artifact implements IBinaryFile {
|
|||
this.artifactPath = path;
|
||||
}
|
||||
|
||||
public getIconName(): string {
|
||||
const extension = pathExtname(this.filePath);
|
||||
|
||||
if (isTextFile(extension)) {
|
||||
return "file-text";
|
||||
}
|
||||
if (isImageFile(extension)) {
|
||||
return "file-image";
|
||||
}
|
||||
if (isAudioFile(extension)) {
|
||||
return "file-music";
|
||||
}
|
||||
if (isVideoFile(extension)) {
|
||||
return "file-play";
|
||||
}
|
||||
if (isKnownFileType(extension)) {
|
||||
return "file";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
public static isArtifactData(this: void, data: unknown): data is {
|
||||
filePath: string;
|
||||
mimeType: string;
|
||||
action: ArtifactAction;
|
||||
originalContent: string;
|
||||
updatedContent: string;
|
||||
base64?: string;
|
||||
|
|
@ -41,6 +77,9 @@ export class Artifact implements IBinaryFile {
|
|||
typeof data.filePath === "string" &&
|
||||
"mimeType" in data &&
|
||||
typeof data.mimeType === "string" &&
|
||||
"action" in data &&
|
||||
typeof data.action === "string" &&
|
||||
isArtifactAction(data.action) &&
|
||||
"originalContent" in data &&
|
||||
typeof data.originalContent === "string" &&
|
||||
"updatedContent" in data &&
|
||||
|
|
|
|||
28
Enums/ArtifactAction.ts
Normal file
28
Enums/ArtifactAction.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Copy } from "Enums/Copy";
|
||||
|
||||
export enum ArtifactAction {
|
||||
Create = "create",
|
||||
Modify = "modify",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export const ARTIFACT_ACTION_RANK = {
|
||||
[ArtifactAction.Create]: 0,
|
||||
[ArtifactAction.Modify]: 1,
|
||||
[ArtifactAction.Delete]: 2,
|
||||
};
|
||||
|
||||
export function isArtifactAction(value: string): value is ArtifactAction {
|
||||
return Object.values(ArtifactAction).includes(value as ArtifactAction);
|
||||
}
|
||||
|
||||
export function artifactActionToCopy(artifactAction: ArtifactAction): string {
|
||||
switch (artifactAction) {
|
||||
case ArtifactAction.Create:
|
||||
return Copy.ArtifactActionCreated;
|
||||
case ArtifactAction.Modify:
|
||||
return Copy.ArtifactActionModified;
|
||||
case ArtifactAction.Delete:
|
||||
return Copy.ArtifactActionDeleted;
|
||||
}
|
||||
}
|
||||
|
|
@ -469,6 +469,11 @@ Each AI provider has their own data policies:
|
|||
// Conversation Modal Copy
|
||||
NoConversationsFound = "No conversations match your search.",
|
||||
|
||||
// Artifact Copy
|
||||
ArtifactActionCreated = "CREATED",
|
||||
ArtifactActionModified = "MODIFIED",
|
||||
ArtifactActionDeleted = "DELETED",
|
||||
|
||||
// Help Modal Additional Copy
|
||||
HelpModalCloseAriaLabel = "Close Help Modal",
|
||||
PluginVersionPrefix = "Plugin version: ",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
} from "AIClasses/Schemas/AIToolSchemas";
|
||||
import { Artifact } from "Conversations/Artifact";
|
||||
import { extname } from "path-browserify";
|
||||
import { ArtifactAction } from "Enums/ArtifactAction";
|
||||
|
||||
export class AIToolService {
|
||||
|
||||
|
|
@ -415,28 +416,6 @@ export class AIToolService {
|
|||
: new AIToolResponsePayload({ path: path, success: true }, artifacts);
|
||||
}
|
||||
|
||||
private async collectDeletionCandidatesArtifacts(filePaths: string[]): Promise<Artifact[]> {
|
||||
const artifacts: Artifact[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const fileType = toFileType(extname(filePath));
|
||||
// Anything that isn't a note we will just store as a binary artifact
|
||||
if (isBinaryFile(fileType) || isDocumentMimeType(FileTypeToMimeType[fileType])) {
|
||||
const result = await this.fileSystemService.readBinaryFile(filePath);
|
||||
if (result instanceof ArrayBuffer) {
|
||||
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], "", "", arrayBufferToBase64(result)));
|
||||
}
|
||||
} else {
|
||||
const result = await this.fileSystemService.readFilePath(filePath);
|
||||
if (typeof result === "string") {
|
||||
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], result, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
private async moveVaultFolder(sourcePath: string, destinationPath: string): Promise<AIToolResponsePayload> {
|
||||
const result = await this.fileSystemService.moveFile(sourcePath, destinationPath);
|
||||
if (result instanceof Error) {
|
||||
|
|
@ -491,10 +470,37 @@ export class AIToolService {
|
|||
return new AIToolResponsePayload({ result: await this.memoriesService.updateMemories(content) });
|
||||
}
|
||||
|
||||
/** Helpers **/
|
||||
|
||||
private async collectDeletionCandidatesArtifacts(filePaths: string[]): Promise<Artifact[]> {
|
||||
const artifacts: Artifact[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const fileType = toFileType(extname(filePath));
|
||||
// Anything that isn't a note we will just store as a binary artifact
|
||||
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)));
|
||||
}
|
||||
} else {
|
||||
const result = await this.fileSystemService.readFilePath(filePath);
|
||||
if (typeof result === "string") {
|
||||
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], ArtifactAction.Delete, result, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
private async asTrackedAction(filePath: string, action: () => Promise<TFile|Error|void>): Promise<AIToolResponsePayload> {
|
||||
let artifactAction: ArtifactAction | undefined;
|
||||
|
||||
let preActionResult = await this.fileSystemService.readFilePath(filePath);
|
||||
if (preActionResult instanceof Error) {
|
||||
preActionResult = ""; // The file does not exist yet
|
||||
artifactAction = ArtifactAction.Create;
|
||||
}
|
||||
const actionResult = await action();
|
||||
if (actionResult instanceof Error) {
|
||||
|
|
@ -503,11 +509,16 @@ export class AIToolService {
|
|||
let postActionResult = actionResult ? await this.fileSystemService.readFile(actionResult) : "";
|
||||
if (postActionResult instanceof Error) {
|
||||
postActionResult = ""; // The file has been deleted
|
||||
artifactAction = ArtifactAction.Delete;
|
||||
}
|
||||
|
||||
if (artifactAction === undefined) {
|
||||
artifactAction = ArtifactAction.Modify;
|
||||
}
|
||||
|
||||
const fileType = toFileType(extname(filePath));
|
||||
|
||||
return new AIToolResponsePayload({ success: true },
|
||||
[new Artifact(filePath, FileTypeToMimeType[fileType], preActionResult, postActionResult)]);
|
||||
[new Artifact(filePath, FileTypeToMimeType[fileType], artifactAction, preActionResult, postActionResult)]);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ export class ConversationFileSystemService {
|
|||
artifacts: content.artifacts.map(artifact => ({
|
||||
filePath: artifact.filePath,
|
||||
mimeType: artifact.mimeType,
|
||||
action: artifact.action,
|
||||
originalContent: artifact.originalContent,
|
||||
updatedContent: artifact.updatedContent,
|
||||
artifactPath: artifact.artifactPath
|
||||
|
|
@ -406,6 +407,7 @@ export class ConversationFileSystemService {
|
|||
artifacts.push(new Artifact(
|
||||
artifactData.filePath,
|
||||
artifactData.mimeType,
|
||||
artifactData.action,
|
||||
artifactData.originalContent,
|
||||
artifactData.updatedContent,
|
||||
base64,
|
||||
|
|
|
|||
|
|
@ -42,12 +42,15 @@ export class VaultCacheService {
|
|||
this.metaDataCache = this.plugin.app.metadataCache;
|
||||
this.registerFileEvents();
|
||||
|
||||
this.plugin.app.metadataCache.on("resolved", async () => {
|
||||
const tryInitialise = async () => {
|
||||
if (!this.initialised) {
|
||||
await this.setupCaches();
|
||||
this.initialised = true;
|
||||
await this.setupCaches();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.plugin.app.metadataCache.on("resolved", tryInitialise);
|
||||
void tryInitialise();
|
||||
}
|
||||
|
||||
public matchTag(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, tag: string }> {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { Artifact } from '../../Conversations/Artifact';
|
||||
import { ArtifactAction } from '../../Enums/ArtifactAction';
|
||||
|
||||
/**
|
||||
* UNIT TESTS - Artifact
|
||||
|
|
@ -11,10 +12,11 @@ import { Artifact } from '../../Conversations/Artifact';
|
|||
describe('Artifact', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an artifact with required fields', () => {
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old content', 'new content');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old content', 'new content');
|
||||
|
||||
expect(artifact.filePath).toBe('notes/test.md');
|
||||
expect(artifact.mimeType).toBe('text/markdown');
|
||||
expect(artifact.action).toBe(ArtifactAction.Modify);
|
||||
expect(artifact.originalContent).toBe('old content');
|
||||
expect(artifact.updatedContent).toBe('new content');
|
||||
expect(artifact.base64).toBeUndefined();
|
||||
|
|
@ -25,6 +27,7 @@ describe('Artifact', () => {
|
|||
const artifact = new Artifact(
|
||||
'image.png',
|
||||
'image/png',
|
||||
ArtifactAction.Modify,
|
||||
'',
|
||||
'',
|
||||
'base64data==',
|
||||
|
|
@ -36,14 +39,14 @@ describe('Artifact', () => {
|
|||
});
|
||||
|
||||
it('should allow empty originalContent for newly created files', () => {
|
||||
const artifact = new Artifact('new-file.md', 'text/markdown', '', 'Some new content');
|
||||
const artifact = new Artifact('new-file.md', 'text/markdown', ArtifactAction.Create, '', 'Some new content');
|
||||
|
||||
expect(artifact.originalContent).toBe('');
|
||||
expect(artifact.updatedContent).toBe('Some new content');
|
||||
});
|
||||
|
||||
it('should allow empty updatedContent for deleted files', () => {
|
||||
const artifact = new Artifact('deleted-file.md', 'text/markdown', 'Content before deletion', '');
|
||||
const artifact = new Artifact('deleted-file.md', 'text/markdown', ArtifactAction.Delete, 'Content before deletion', '');
|
||||
|
||||
expect(artifact.originalContent).toBe('Content before deletion');
|
||||
expect(artifact.updatedContent).toBe('');
|
||||
|
|
@ -52,19 +55,19 @@ describe('Artifact', () => {
|
|||
|
||||
describe('getStoragePath / setStoragePath', () => {
|
||||
it('should return undefined when no storage path has been set', () => {
|
||||
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b');
|
||||
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b');
|
||||
|
||||
expect(artifact.getStoragePath()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the artifactPath passed to the constructor', () => {
|
||||
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b', 'base64', 'Artifacts/existing.bin');
|
||||
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b', 'base64', 'Artifacts/existing.bin');
|
||||
|
||||
expect(artifact.getStoragePath()).toBe('Artifacts/existing.bin');
|
||||
});
|
||||
|
||||
it('should update the storage path when set', () => {
|
||||
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b');
|
||||
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b');
|
||||
|
||||
artifact.setStoragePath('Artifacts/newly-saved.bin');
|
||||
|
||||
|
|
@ -73,7 +76,7 @@ describe('Artifact', () => {
|
|||
});
|
||||
|
||||
it('should overwrite an existing storage path', () => {
|
||||
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b', undefined, 'Artifacts/old.bin');
|
||||
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b', undefined, 'Artifacts/old.bin');
|
||||
|
||||
artifact.setStoragePath('Artifacts/new.bin');
|
||||
|
||||
|
|
@ -86,6 +89,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
|
@ -97,6 +101,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
base64: 'YWJj',
|
||||
|
|
@ -129,6 +134,30 @@ describe('Artifact', () => {
|
|||
it('should return false when mimeType is missing', () => {
|
||||
const data = {
|
||||
filePath: 'test.md',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
||||
expect(Artifact.isArtifactData(data)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when action is missing', () => {
|
||||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
||||
expect(Artifact.isArtifactData(data)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when action is not a valid ArtifactAction', () => {
|
||||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: 'not-a-valid-action',
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
|
@ -140,6 +169,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
||||
|
|
@ -150,6 +180,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old'
|
||||
};
|
||||
|
||||
|
|
@ -160,6 +191,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 123,
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
};
|
||||
|
|
@ -171,6 +203,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
base64: 12345
|
||||
|
|
@ -183,6 +216,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
artifactPath: true
|
||||
|
|
@ -195,6 +229,7 @@ describe('Artifact', () => {
|
|||
const data = {
|
||||
filePath: 'new-file.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Create,
|
||||
originalContent: '',
|
||||
updatedContent: ''
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Role } from '../../Enums/Role';
|
|||
import { TFile } from 'obsidian';
|
||||
import { Exception } from '../../Helpers/Exception';
|
||||
import { Artifact } from '../../Conversations/Artifact';
|
||||
import { ArtifactAction } from '../../Enums/ArtifactAction';
|
||||
|
||||
/**
|
||||
* INTEGRATION TESTS - ConversationFileSystemService
|
||||
|
|
@ -533,7 +534,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
describe('Artifacts', () => {
|
||||
it('should save an artifact with base64 content as a binary file and store its path', async () => {
|
||||
const conversation = createTestConversation('With Artifact');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new', 'YWJjZGVm');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new', 'YWJjZGVm');
|
||||
|
||||
conversation.contents.push(
|
||||
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
|
||||
|
|
@ -554,6 +555,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
expect(savedArtifact).toMatchObject({
|
||||
filePath: 'notes/test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
artifactPath: artifact.artifactPath
|
||||
|
|
@ -562,7 +564,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
|
||||
it('should not write a binary file for artifacts without base64 content (text-only edits)', async () => {
|
||||
const conversation = createTestConversation('Text Only Artifact');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new');
|
||||
|
||||
conversation.contents.push(
|
||||
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
|
||||
|
|
@ -576,7 +578,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
|
||||
it('should not re-write a binary file for an artifact that already has a storage path', async () => {
|
||||
const conversation = createTestConversation('Existing Artifact');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new', 'YWJjZGVm', 'Artifacts/existing-hash.bin');
|
||||
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new', 'YWJjZGVm', 'Artifacts/existing-hash.bin');
|
||||
|
||||
conversation.contents.push(
|
||||
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
|
||||
|
|
@ -604,6 +606,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
{
|
||||
filePath: 'notes/test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
artifactPath: 'Artifacts/hash123.bin'
|
||||
|
|
@ -644,6 +647,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
{
|
||||
filePath: 'notes/test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new'
|
||||
}
|
||||
|
|
@ -707,6 +711,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
{
|
||||
filePath: 'notes/test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
artifactPath: 'Artifacts/referenced.bin'
|
||||
|
|
@ -758,6 +763,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
{
|
||||
filePath: 'notes/test.md',
|
||||
mimeType: 'text/markdown',
|
||||
action: ArtifactAction.Modify,
|
||||
originalContent: 'old',
|
||||
updatedContent: 'new',
|
||||
artifactPath: 'Artifacts/kept.bin'
|
||||
|
|
|
|||
Loading…
Reference in a new issue