mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-08 10:33:44 +00:00
Compare commits
25 Commits
0.23.0-bet
...
v0.23.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec4bbb298 | ||
|
|
812c199b45 | ||
|
|
36bd8f645a | ||
|
|
7cff8091e4 | ||
|
|
de8feb98a3 | ||
|
|
fbd6e8fa97 | ||
|
|
bcf6bd1dfc | ||
|
|
8627560fd5 | ||
|
|
9a3e44c6d6 | ||
|
|
7b53641a94 | ||
|
|
3948b8eada | ||
|
|
d05bb9992c | ||
|
|
b2c09825ac | ||
|
|
65453c31c6 | ||
|
|
d9e8ce802f | ||
|
|
d5f63b9e43 | ||
|
|
ebefbeefc8 | ||
|
|
4d7d8f215f | ||
|
|
b6187718ea | ||
|
|
3ee82bd9ce | ||
|
|
3dbdb99435 | ||
|
|
0d414d914a | ||
|
|
41f338bce0 | ||
|
|
6f87c1ca50 | ||
|
|
33f6496d79 |
@@ -85,6 +85,8 @@ export class MenuSubMenu extends MenuFocusable {
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
this.menu.openSubMenu(menu);
|
||||
// in case that the menu is not closed, but the component is removed,
|
||||
this.disposables.add(unsub);
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
|
||||
@@ -18,6 +18,7 @@ export const LoadingIcon = ({
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
style="fill: none;"
|
||||
>
|
||||
<style>
|
||||
.spinner {
|
||||
|
||||
@@ -116,6 +116,7 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
|
||||
`;
|
||||
|
||||
private _cleanup: (() => void) | null = null;
|
||||
private _autoUpdateCleanup: (() => void) | null = null;
|
||||
|
||||
private _prevTool: ToolOptionWithType | null = null;
|
||||
|
||||
@@ -128,6 +129,11 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
|
||||
return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]];
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.disposables.add(() => this._autoUpdateCleanup?.());
|
||||
}
|
||||
|
||||
private _closePanel() {
|
||||
if (this._openedPanel) {
|
||||
this._openedPanel.remove();
|
||||
@@ -175,8 +181,8 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement;
|
||||
|
||||
autoUpdate(this, panel, () => {
|
||||
this._autoUpdateCleanup?.();
|
||||
this._autoUpdateCleanup = autoUpdate(this, panel, () => {
|
||||
computePosition(this, panel, {
|
||||
placement: 'top',
|
||||
middleware: [offset(20), arrow({ element: arrowEl }), shift()],
|
||||
|
||||
@@ -22,8 +22,11 @@ import { isEqual } from 'lodash-es';
|
||||
})
|
||||
export class InlineComment extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
inline-comment {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
inline-comment.unresolved {
|
||||
display: inline-block;
|
||||
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
|
||||
border-bottom: 2px solid
|
||||
${unsafeCSSVarV2('block/comment/highlightUnderline')};
|
||||
|
||||
@@ -372,3 +372,66 @@ Generated by [AVA](https://avajs.dev).
|
||||
[assistant]: Quantum computing uses quantum mechanics principles.`,
|
||||
promptName: 'Summary as title',
|
||||
}
|
||||
|
||||
## should handle copilot cron jobs correctly
|
||||
|
||||
> daily job scheduling calls
|
||||
|
||||
[
|
||||
{
|
||||
args: [
|
||||
'copilot.session.cleanupEmptySessions',
|
||||
{},
|
||||
{
|
||||
jobId: 'daily-copilot-cleanup-empty-sessions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
args: [
|
||||
'copilot.session.generateMissingTitles',
|
||||
{},
|
||||
{
|
||||
jobId: 'daily-copilot-generate-missing-titles',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
> cleanup empty sessions calls
|
||||
|
||||
[
|
||||
{
|
||||
args: [
|
||||
'Date',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
> title generation calls
|
||||
|
||||
{
|
||||
jobCalls: [
|
||||
{
|
||||
args: [
|
||||
'copilot.session.generateTitle',
|
||||
{
|
||||
sessionId: 'session1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
args: [
|
||||
'copilot.session.generateTitle',
|
||||
{
|
||||
sessionId: 'session2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
modelCalls: [
|
||||
{
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -351,10 +351,10 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
blobId: 'euclidean_distance',
|
||||
fileName: 'euclidean_distance.rs',
|
||||
fileType: 'text/rust',
|
||||
fileContent: TestAssets.Code,
|
||||
blobId: 'todo_md',
|
||||
fileName: 'todo.md',
|
||||
fileType: 'text/markdown',
|
||||
fileContent: TestAssets.TODO,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -476,6 +476,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
},
|
||||
},
|
||||
],
|
||||
config: { model: 'gemini-2.5-pro' },
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
t.notThrows(() => {
|
||||
TranscriptionResponseSchema.parse(JSON.parse(result));
|
||||
@@ -697,11 +698,12 @@ for (const {
|
||||
t.truthy(provider, 'should have provider');
|
||||
await retry(`action: ${promptName}`, t, async t => {
|
||||
const finalConfig = Object.assign({}, prompt.config, config);
|
||||
const modelId = finalConfig.model || prompt.model;
|
||||
|
||||
switch (type) {
|
||||
case 'text': {
|
||||
const result = await provider.text(
|
||||
{ modelId: prompt.model },
|
||||
{ modelId },
|
||||
[
|
||||
...prompt.finish(
|
||||
messages.reduce(
|
||||
@@ -720,7 +722,7 @@ for (const {
|
||||
}
|
||||
case 'structured': {
|
||||
const result = await provider.structure(
|
||||
{ modelId: prompt.model },
|
||||
{ modelId },
|
||||
[
|
||||
...prompt.finish(
|
||||
messages.reduce(
|
||||
@@ -739,7 +741,7 @@ for (const {
|
||||
case 'object': {
|
||||
const streamObjects: StreamObject[] = [];
|
||||
for await (const chunk of provider.streamObject(
|
||||
{ modelId: prompt.model },
|
||||
{ modelId },
|
||||
[
|
||||
...prompt.finish(
|
||||
messages.reduce(
|
||||
@@ -771,7 +773,7 @@ for (const {
|
||||
});
|
||||
}
|
||||
const stream = provider.streamImages(
|
||||
{ modelId: prompt.model },
|
||||
{ modelId },
|
||||
[
|
||||
...prompt.finish(
|
||||
finalMessage.reduce(
|
||||
|
||||
@@ -290,6 +290,7 @@ test('should fork session correctly', async t => {
|
||||
|
||||
const assertForkSession = async (
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
sessionId: string,
|
||||
lastMessageId: string | undefined,
|
||||
error: string,
|
||||
@@ -300,13 +301,7 @@ test('should fork session correctly', async t => {
|
||||
}
|
||||
) =>
|
||||
await asserter(
|
||||
forkCopilotSession(
|
||||
app,
|
||||
workspaceId,
|
||||
randomUUID(),
|
||||
sessionId,
|
||||
lastMessageId
|
||||
)
|
||||
forkCopilotSession(app, workspaceId, docId, sessionId, lastMessageId)
|
||||
);
|
||||
|
||||
// prepare session
|
||||
@@ -330,6 +325,7 @@ test('should fork session correctly', async t => {
|
||||
// should be able to fork session
|
||||
forkedSessionId = await assertForkSession(
|
||||
id,
|
||||
docId,
|
||||
sessionId,
|
||||
latestMessageId!,
|
||||
'should be able to fork session with cloud workspace that user can access'
|
||||
@@ -340,6 +336,7 @@ test('should fork session correctly', async t => {
|
||||
{
|
||||
forkedSessionId = await assertForkSession(
|
||||
id,
|
||||
docId,
|
||||
sessionId,
|
||||
undefined,
|
||||
'should be able to fork session without latestMessageId'
|
||||
@@ -348,18 +345,25 @@ test('should fork session correctly', async t => {
|
||||
|
||||
// should not be able to fork session with wrong latestMessageId
|
||||
{
|
||||
await assertForkSession(id, sessionId, 'wrong-message-id', '', async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
{ instanceOf: Error },
|
||||
'should not able to fork session with wrong latestMessageId'
|
||||
);
|
||||
});
|
||||
await assertForkSession(
|
||||
id,
|
||||
docId,
|
||||
sessionId,
|
||||
'wrong-message-id',
|
||||
'',
|
||||
async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
{ instanceOf: Error },
|
||||
'should not able to fork session with wrong latestMessageId'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const u2 = await app.signupV1();
|
||||
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
|
||||
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
{ instanceOf: Error },
|
||||
@@ -371,7 +375,7 @@ test('should fork session correctly', async t => {
|
||||
const inviteId = await inviteUser(app, id, u2.email);
|
||||
await app.switchUser(u2);
|
||||
await acceptInviteById(app, id, inviteId, false);
|
||||
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
|
||||
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
{ instanceOf: Error },
|
||||
@@ -389,6 +393,7 @@ test('should fork session correctly', async t => {
|
||||
await app.switchUser(u2);
|
||||
await assertForkSession(
|
||||
id,
|
||||
docId,
|
||||
forkedSessionId,
|
||||
latestMessageId!,
|
||||
'should able to fork a forked session created by other user'
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../models';
|
||||
import { CopilotModule } from '../plugins/copilot';
|
||||
import { CopilotContextService } from '../plugins/copilot/context';
|
||||
import { CopilotCronJobs } from '../plugins/copilot/cron';
|
||||
import {
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
@@ -77,6 +78,7 @@ type Context = {
|
||||
jobs: CopilotEmbeddingJob;
|
||||
storage: CopilotStorage;
|
||||
workflow: CopilotWorkflowService;
|
||||
cronJobs: CopilotCronJobs;
|
||||
executors: {
|
||||
image: CopilotChatImageExecutor;
|
||||
text: CopilotChatTextExecutor;
|
||||
@@ -137,6 +139,7 @@ test.before(async t => {
|
||||
const jobs = module.get(CopilotEmbeddingJob);
|
||||
const transcript = module.get(CopilotTranscriptionService);
|
||||
const workspaceEmbedding = module.get(CopilotWorkspaceService);
|
||||
const cronJobs = module.get(CopilotCronJobs);
|
||||
|
||||
t.context.module = module;
|
||||
t.context.auth = auth;
|
||||
@@ -153,6 +156,7 @@ test.before(async t => {
|
||||
t.context.jobs = jobs;
|
||||
t.context.transcript = transcript;
|
||||
t.context.workspaceEmbedding = workspaceEmbedding;
|
||||
t.context.cronJobs = cronJobs;
|
||||
|
||||
t.context.executors = {
|
||||
image: module.get(CopilotChatImageExecutor),
|
||||
@@ -1931,3 +1935,71 @@ test('should handle generateSessionTitle correctly under various conditions', as
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle copilot cron jobs correctly', async t => {
|
||||
const { cronJobs, copilotSession } = t.context;
|
||||
|
||||
// mock calls
|
||||
const mockCleanupResult = { removed: 2, cleaned: 3 };
|
||||
const mockSessions = [
|
||||
{ id: 'session1', _count: { messages: 1 } },
|
||||
{ id: 'session2', _count: { messages: 2 } },
|
||||
];
|
||||
const cleanupStub = Sinon.stub(
|
||||
copilotSession,
|
||||
'cleanupEmptySessions'
|
||||
).resolves(mockCleanupResult);
|
||||
const toBeGenerateStub = Sinon.stub(
|
||||
copilotSession,
|
||||
'toBeGenerateTitle'
|
||||
).resolves(mockSessions);
|
||||
const jobAddStub = Sinon.stub(cronJobs['jobs'], 'add').resolves();
|
||||
|
||||
// daily cleanup job scheduling
|
||||
{
|
||||
await cronJobs.dailyCleanupJob();
|
||||
t.snapshot(
|
||||
jobAddStub.getCalls().map(call => ({
|
||||
args: call.args,
|
||||
})),
|
||||
'daily job scheduling calls'
|
||||
);
|
||||
|
||||
jobAddStub.reset();
|
||||
cleanupStub.reset();
|
||||
toBeGenerateStub.reset();
|
||||
}
|
||||
|
||||
// cleanup empty sessions
|
||||
{
|
||||
// mock
|
||||
cleanupStub.resolves(mockCleanupResult);
|
||||
toBeGenerateStub.resolves(mockSessions);
|
||||
|
||||
await cronJobs.cleanupEmptySessions();
|
||||
t.snapshot(
|
||||
cleanupStub.getCalls().map(call => ({
|
||||
args: call.args.map(arg => (arg instanceof Date ? 'Date' : arg)), // Replace Date with string for stable snapshot
|
||||
})),
|
||||
'cleanup empty sessions calls'
|
||||
);
|
||||
}
|
||||
|
||||
// generate missing titles
|
||||
await cronJobs.generateMissingTitles();
|
||||
t.snapshot(
|
||||
{
|
||||
modelCalls: toBeGenerateStub.getCalls().map(call => ({
|
||||
args: call.args,
|
||||
})),
|
||||
jobCalls: jobAddStub.getCalls().map(call => ({
|
||||
args: call.args,
|
||||
})),
|
||||
},
|
||||
'title generation calls'
|
||||
);
|
||||
|
||||
cleanupStub.restore();
|
||||
toBeGenerateStub.restore();
|
||||
jobAddStub.restore();
|
||||
});
|
||||
|
||||
@@ -111,6 +111,19 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.5-pro',
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [
|
||||
ModelOutputType.Text,
|
||||
ModelOutputType.Object,
|
||||
ModelOutputType.Structured,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
override async text(
|
||||
|
||||
@@ -565,3 +565,65 @@ Generated by [AVA](https://avajs.dev).
|
||||
workspaceSessionExists: true,
|
||||
},
|
||||
}
|
||||
|
||||
## should cleanup empty sessions correctly
|
||||
|
||||
> cleanup empty sessions results
|
||||
|
||||
{
|
||||
cleanupResult: {
|
||||
cleaned: 0,
|
||||
removed: 0,
|
||||
},
|
||||
remainingSessions: [
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'zeroCost',
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'zeroCost',
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'noMessages',
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'noMessages',
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'recent',
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
pinned: false,
|
||||
type: 'withMessages',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
## should get sessions for title generation correctly
|
||||
|
||||
> sessions for title generation results
|
||||
|
||||
{
|
||||
onlyValidSessionsReturned: true,
|
||||
sessions: [
|
||||
{
|
||||
assistantMessageCount: 1,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
assistantMessageCount: 2,
|
||||
isValid: true,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -917,3 +917,178 @@ test('should handle fork and session attachment operations', async t => {
|
||||
'attach and detach operation results'
|
||||
);
|
||||
});
|
||||
|
||||
test('should cleanup empty sessions correctly', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
||||
|
||||
// should be deleted
|
||||
const neverUsedSessionIds: string[] = [randomUUID(), randomUUID()];
|
||||
await Promise.all(
|
||||
neverUsedSessionIds.map(async id => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSession.update({
|
||||
where: { id },
|
||||
data: { messageCost: 0, updatedAt: oneDayAgo },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// should be marked as deleted
|
||||
const emptySessionIds: string[] = [randomUUID(), randomUUID()];
|
||||
await Promise.all(
|
||||
emptySessionIds.map(async id => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSession.update({
|
||||
where: { id },
|
||||
data: { messageCost: 100, updatedAt: oneDayAgo },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// should not be affected
|
||||
const recentSessionId = randomUUID();
|
||||
await createTestSession(t, { sessionId: recentSessionId });
|
||||
await db.aiSession.update({
|
||||
where: { id: recentSessionId },
|
||||
data: { messageCost: 0, updatedAt: twoHoursAgo },
|
||||
});
|
||||
|
||||
// Create session with messages (should not be affected)
|
||||
const sessionWithMsgId = randomUUID();
|
||||
await createSessionWithMessages(
|
||||
t,
|
||||
{ sessionId: sessionWithMsgId },
|
||||
'test message'
|
||||
);
|
||||
|
||||
const result = await copilotSession.cleanupEmptySessions(oneDayAgo);
|
||||
|
||||
const remainingSessions = await db.aiSession.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [
|
||||
...neverUsedSessionIds,
|
||||
...emptySessionIds,
|
||||
recentSessionId,
|
||||
sessionWithMsgId,
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { id: true, deletedAt: true, pinned: true },
|
||||
});
|
||||
|
||||
t.snapshot(
|
||||
{
|
||||
cleanupResult: result,
|
||||
remainingSessions: remainingSessions.map(s => ({
|
||||
deleted: !!s.deletedAt,
|
||||
pinned: s.pinned,
|
||||
type: neverUsedSessionIds.includes(s.id)
|
||||
? 'zeroCost'
|
||||
: emptySessionIds.includes(s.id)
|
||||
? 'noMessages'
|
||||
: s.id === recentSessionId
|
||||
? 'recent'
|
||||
: 'withMessages',
|
||||
})),
|
||||
},
|
||||
'cleanup empty sessions results'
|
||||
);
|
||||
});
|
||||
|
||||
test('should get sessions for title generation correctly', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
// create valid sessions with messages
|
||||
const sessionIds: string[] = [randomUUID(), randomUUID()];
|
||||
await Promise.all(
|
||||
sessionIds.map(async (id, index) => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSession.update({
|
||||
where: { id },
|
||||
data: {
|
||||
updatedAt: new Date(Date.now() - index * 1000),
|
||||
messages: {
|
||||
create: Array.from({ length: index + 1 }, (_, i) => ({
|
||||
role: 'assistant',
|
||||
content: `assistant message ${i}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// create excluded sessions
|
||||
const excludedSessions = [
|
||||
{
|
||||
reason: 'hasTitle',
|
||||
setupFn: async (id: string) => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSession.update({
|
||||
where: { id },
|
||||
data: { title: 'Existing Title' },
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
reason: 'isDeleted',
|
||||
setupFn: async (id: string) => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSession.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
reason: 'noMessages',
|
||||
setupFn: async (id: string) => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
},
|
||||
},
|
||||
{
|
||||
reason: 'isAction',
|
||||
setupFn: async (id: string) => {
|
||||
await createTestSession(t, {
|
||||
sessionId: id,
|
||||
promptName: TEST_PROMPTS.ACTION,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
reason: 'noAssistantMessages',
|
||||
setupFn: async (id: string) => {
|
||||
await createTestSession(t, { sessionId: id });
|
||||
await db.aiSessionMessage.create({
|
||||
data: { sessionId: id, role: 'user', content: 'User message only' },
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
excludedSessions.map(async session => {
|
||||
await session.setupFn(randomUUID());
|
||||
})
|
||||
);
|
||||
|
||||
const result = await copilotSession.toBeGenerateTitle();
|
||||
|
||||
t.snapshot(
|
||||
{
|
||||
total: result.length,
|
||||
sessions: result.map(s => ({
|
||||
assistantMessageCount: s._count.messages,
|
||||
isValid: sessionIds.includes(s.id),
|
||||
})),
|
||||
onlyValidSessionsReturned: result.every(s => sessionIds.includes(s.id)),
|
||||
},
|
||||
'sessions for title generation results'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -433,7 +433,7 @@ export async function submitAudioTranscription(
|
||||
for (const [idx, buffer] of content.entries()) {
|
||||
resp = resp.attach(idx.toString(), buffer, {
|
||||
filename: fileName,
|
||||
contentType: 'application/octet-stream',
|
||||
contentType: 'audio/opus',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -582,4 +582,56 @@ export class CopilotSessionModel extends BaseModel {
|
||||
.map(({ messageCost, prompt: { action } }) => (action ? 1 : messageCost))
|
||||
.reduce((prev, cost) => prev + cost, 0);
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async cleanupEmptySessions(earlyThen: Date) {
|
||||
// delete never used sessions
|
||||
const { count: removed } = await this.db.aiSession.deleteMany({
|
||||
where: {
|
||||
messageCost: 0,
|
||||
deletedAt: null,
|
||||
// filter session updated more than 24 hours ago
|
||||
updatedAt: { lt: earlyThen },
|
||||
},
|
||||
});
|
||||
|
||||
// mark empty sessions as deleted
|
||||
const { count: cleaned } = await this.db.aiSession.updateMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
messages: { none: {} },
|
||||
// filter session updated more than 24 hours ago
|
||||
updatedAt: { lt: earlyThen },
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
pinned: false,
|
||||
},
|
||||
});
|
||||
|
||||
return { removed, cleaned };
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async toBeGenerateTitle() {
|
||||
const sessions = await this.db.aiSession
|
||||
.findMany({
|
||||
where: {
|
||||
title: null,
|
||||
deletedAt: null,
|
||||
messages: { some: {} },
|
||||
// only generate titles for non-actions sessions
|
||||
prompt: { action: null },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
// count assistant messages
|
||||
_count: { select: { messages: { where: { role: 'assistant' } } } },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
})
|
||||
.then(s => s.filter(s => s._count.messages > 0));
|
||||
|
||||
return sessions;
|
||||
}
|
||||
}
|
||||
|
||||
71
packages/backend/server/src/plugins/copilot/cron.ts
Normal file
71
packages/backend/server/src/plugins/copilot/cron.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
|
||||
import { JobQueue, OneDay, OnJob } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
|
||||
declare global {
|
||||
interface Jobs {
|
||||
'copilot.session.cleanupEmptySessions': {};
|
||||
'copilot.session.generateMissingTitles': {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CopilotCronJobs {
|
||||
private readonly logger = new Logger(CopilotCronJobs.name);
|
||||
|
||||
constructor(
|
||||
private readonly models: Models,
|
||||
private readonly jobs: JobQueue
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
async dailyCleanupJob() {
|
||||
await this.jobs.add(
|
||||
'copilot.session.cleanupEmptySessions',
|
||||
{},
|
||||
{ jobId: 'daily-copilot-cleanup-empty-sessions' }
|
||||
);
|
||||
|
||||
await this.jobs.add(
|
||||
'copilot.session.generateMissingTitles',
|
||||
{},
|
||||
{ jobId: 'daily-copilot-generate-missing-titles' }
|
||||
);
|
||||
}
|
||||
|
||||
async triggerGenerateMissingTitles() {
|
||||
await this.jobs.add(
|
||||
'copilot.session.generateMissingTitles',
|
||||
{},
|
||||
{ jobId: 'trigger-copilot-generate-missing-titles' }
|
||||
);
|
||||
}
|
||||
|
||||
@OnJob('copilot.session.cleanupEmptySessions')
|
||||
async cleanupEmptySessions() {
|
||||
const { removed, cleaned } =
|
||||
await this.models.copilotSession.cleanupEmptySessions(
|
||||
new Date(Date.now() - OneDay)
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Cleanup completed: ${removed} sessions deleted, ${cleaned} sessions marked as deleted`
|
||||
);
|
||||
}
|
||||
|
||||
@OnJob('copilot.session.generateMissingTitles')
|
||||
async generateMissingTitles() {
|
||||
const sessions = await this.models.copilotSession.toBeGenerateTitle();
|
||||
|
||||
for (const session of sessions) {
|
||||
await this.jobs.add('copilot.session.generateTitle', {
|
||||
sessionId: session.id,
|
||||
});
|
||||
}
|
||||
this.logger.log(
|
||||
`Scheduled title generation for ${sessions.length} sessions`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CopilotContextService,
|
||||
} from './context';
|
||||
import { CopilotController } from './controller';
|
||||
import { CopilotCronJobs } from './cron';
|
||||
import { CopilotEmbeddingJob } from './embedding';
|
||||
import { ChatMessageCache } from './message';
|
||||
import { PromptService } from './prompt';
|
||||
@@ -64,6 +65,8 @@ import {
|
||||
CopilotContextResolver,
|
||||
CopilotContextService,
|
||||
CopilotEmbeddingJob,
|
||||
// cron jobs
|
||||
CopilotCronJobs,
|
||||
// transcription
|
||||
CopilotTranscriptionService,
|
||||
CopilotTranscriptionResolver,
|
||||
|
||||
@@ -303,7 +303,8 @@ const textActions: Prompt[] = [
|
||||
{
|
||||
name: 'Transcript audio',
|
||||
action: 'Transcript audio',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-pro',
|
||||
optionalModels: ['gemini-2.5-flash', 'gemini-2.5-pro'],
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1623,6 +1624,166 @@ const imageActions: Prompt[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const modelActions: Prompt[] = [
|
||||
{
|
||||
name: 'Apply Updates',
|
||||
action: 'Apply Updates',
|
||||
model: 'claude-sonnet-4@20250514',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
You are a Markdown document update engine.
|
||||
|
||||
You will be given:
|
||||
|
||||
1. content: The original Markdown document
|
||||
- The content is structured into blocks.
|
||||
- Each block starts with a comment like <!-- block_id=... flavour=... --> and contains the block's content.
|
||||
- The content is {{content}}
|
||||
|
||||
2. op: A description of the edit intention
|
||||
- This describes the semantic meaning of the edit, such as "Bold the first paragraph".
|
||||
- The op is {{op}}
|
||||
|
||||
3. updates: A Markdown snippet
|
||||
- The updates is {{updates}}
|
||||
- This represents the block-level changes to apply to the original Markdown.
|
||||
- The update may:
|
||||
- **Replace** an existing block (same block_id, new content)
|
||||
- **Delete** block(s) using <!-- delete block BLOCK_ID -->
|
||||
- **Insert** new block(s) with a new unique block_id
|
||||
- When performing deletions, the update will include **surrounding context blocks** (or use <!-- existing blocks -->) to help you determine where and what to delete.
|
||||
|
||||
Your task:
|
||||
- Apply the update in <updates> to the document in <code>, following the intent described in <op>.
|
||||
- Preserve all block_id and flavour comments.
|
||||
- Maintain the original block order unless the update clearly appends new blocks.
|
||||
- Do not remove or alter unrelated blocks.
|
||||
- Output only the fully updated Markdown content. Do not wrap the content in \`\`\`markdown.
|
||||
|
||||
---
|
||||
|
||||
✍️ Examples
|
||||
|
||||
✅ Replacement (modifying an existing block)
|
||||
|
||||
<code>
|
||||
<!-- block_id=101 flavour=paragraph -->
|
||||
## Introduction
|
||||
|
||||
<!-- block_id=102 flavour=paragraph -->
|
||||
This document provides an overview of the system architecture and its components.
|
||||
</code>
|
||||
|
||||
<op>
|
||||
Make the introduction more formal.
|
||||
</op>
|
||||
|
||||
<updates>
|
||||
<!-- block_id=102 flavour=paragraph -->
|
||||
This document outlines the architectural design and individual components of the system in detail.
|
||||
</updates>
|
||||
|
||||
Expected Output:
|
||||
<!-- block_id=101 flavour=paragraph -->
|
||||
## Introduction
|
||||
|
||||
<!-- block_id=102 flavour=paragraph -->
|
||||
This document outlines the architectural design and individual components of the system in detail.
|
||||
|
||||
---
|
||||
|
||||
➕ Insertion (adding new content)
|
||||
|
||||
<code>
|
||||
<!-- block_id=201 flavour=paragraph -->
|
||||
# Project Summary
|
||||
|
||||
<!-- block_id=202 flavour=paragraph -->
|
||||
This project aims to build a collaborative text editing tool.
|
||||
</code>
|
||||
|
||||
<op>
|
||||
Add a disclaimer section at the end.
|
||||
</op>
|
||||
|
||||
<updates>
|
||||
<!-- block_id=new-301 flavour=paragraph -->
|
||||
## Disclaimer
|
||||
|
||||
<!-- block_id=new-302 flavour=paragraph -->
|
||||
This document is subject to change. Do not distribute externally.
|
||||
</updates>
|
||||
|
||||
Expected Output:
|
||||
<!-- block_id=201 flavour=paragraph -->
|
||||
# Project Summary
|
||||
|
||||
<!-- block_id=202 flavour=paragraph -->
|
||||
This project aims to build a collaborative text editing tool.
|
||||
|
||||
<!-- block_id=new-301 flavour=paragraph -->
|
||||
## Disclaimer
|
||||
|
||||
<!-- block_id=new-302 flavour=paragraph -->
|
||||
This document is subject to change. Do not distribute externally.
|
||||
|
||||
---
|
||||
|
||||
❌ Deletion (removing blocks)
|
||||
|
||||
<code>
|
||||
<!-- block_id=401 flavour=paragraph -->
|
||||
## Author
|
||||
|
||||
<!-- block_id=402 flavour=paragraph -->
|
||||
Written by the AI team at OpenResearch.
|
||||
|
||||
<!-- block_id=403 flavour=paragraph -->
|
||||
## Experimental Section
|
||||
|
||||
<!-- block_id=404 flavour=paragraph -->
|
||||
The following section is still under development and may change without notice.
|
||||
|
||||
<!-- block_id=405 flavour=paragraph -->
|
||||
## License
|
||||
|
||||
<!-- block_id=406 flavour=paragraph -->
|
||||
This document is licensed under CC BY-NC 4.0.
|
||||
</code>
|
||||
|
||||
<op>
|
||||
Remove the experimental section.
|
||||
</op>
|
||||
|
||||
<updates>
|
||||
<!-- delete block_id=403 -->
|
||||
<!-- delete block_id=404 -->
|
||||
</updates>
|
||||
|
||||
Expected Output:
|
||||
<!-- block_id=401 flavour=paragraph -->
|
||||
## Author
|
||||
|
||||
<!-- block_id=402 flavour=paragraph -->
|
||||
Written by the AI team at OpenResearch.
|
||||
|
||||
<!-- block_id=405 flavour=paragraph -->
|
||||
## License
|
||||
|
||||
<!-- block_id=406 flavour=paragraph -->
|
||||
This document is licensed under CC BY-NC 4.0.
|
||||
|
||||
---
|
||||
|
||||
Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, and return the updated Markdown.
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
|
||||
model: 'claude-sonnet-4@20250514',
|
||||
optionalModels: [
|
||||
@@ -1860,6 +2021,7 @@ const artifactActions: Prompt[] = [
|
||||
export const prompts: Prompt[] = [
|
||||
...textActions,
|
||||
...imageActions,
|
||||
...modelActions,
|
||||
...chat,
|
||||
...workflows,
|
||||
...artifactActions,
|
||||
|
||||
@@ -37,6 +37,24 @@ export class MorphProvider extends CopilotProvider<MorphConfig> {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'morph-v3-fast',
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'morph-v3-large',
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text],
|
||||
output: [ModelOutputType.Text],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
#instance!: VercelOpenAICompatibleProvider;
|
||||
|
||||
@@ -172,6 +172,7 @@ export abstract class CopilotProvider<C = any> {
|
||||
const getDocContent = buildContentGetter(ac, docReader);
|
||||
tools.doc_edit = createDocEditTool(
|
||||
this.factory,
|
||||
prompt,
|
||||
getDocContent.bind(null, options)
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -472,10 +472,18 @@ export class TextStreamParser {
|
||||
result = this.addPrefix(result);
|
||||
switch (chunk.toolName) {
|
||||
case 'doc_edit': {
|
||||
if (chunk.result && typeof chunk.result === 'object') {
|
||||
result += `\n${chunk.result.result}\n`;
|
||||
if (
|
||||
chunk.result &&
|
||||
typeof chunk.result === 'object' &&
|
||||
Array.isArray(chunk.result.result)
|
||||
) {
|
||||
result += chunk.result.result
|
||||
.map(item => {
|
||||
return `\n${item.changedContent}\n`;
|
||||
})
|
||||
.join('');
|
||||
this.docEditFootnotes[this.docEditFootnotes.length - 1].result =
|
||||
chunk.result.result;
|
||||
result;
|
||||
} else {
|
||||
this.docEditFootnotes.pop();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
CallMetric,
|
||||
CopilotDocNotFound,
|
||||
CopilotFailedToCreateMessage,
|
||||
CopilotProviderSideError,
|
||||
CopilotSessionNotFound,
|
||||
type FileUpload,
|
||||
paginate,
|
||||
@@ -31,14 +32,18 @@ import {
|
||||
RequestMutex,
|
||||
Throttle,
|
||||
TooManyRequest,
|
||||
UserFriendlyError,
|
||||
} from '../../base';
|
||||
import { CurrentUser } from '../../core/auth';
|
||||
import { Admin } from '../../core/common';
|
||||
import { DocReader } from '../../core/doc';
|
||||
import { AccessController } from '../../core/permission';
|
||||
import { UserType } from '../../core/user';
|
||||
import type { ListSessionOptions, UpdateChatSession } from '../../models';
|
||||
import { CopilotCronJobs } from './cron';
|
||||
import { PromptService } from './prompt';
|
||||
import { PromptMessage, StreamObject } from './providers';
|
||||
import { CopilotProviderFactory } from './providers/factory';
|
||||
import { ChatSessionService } from './session';
|
||||
import { CopilotStorage } from './storage';
|
||||
import { type ChatHistory, type ChatMessage, SubmittedMessage } from './types';
|
||||
@@ -396,7 +401,9 @@ export class CopilotResolver {
|
||||
private readonly ac: AccessController,
|
||||
private readonly mutex: RequestMutex,
|
||||
private readonly chatSession: ChatSessionService,
|
||||
private readonly storage: CopilotStorage
|
||||
private readonly storage: CopilotStorage,
|
||||
private readonly docReader: DocReader,
|
||||
private readonly providerFactory: CopilotProviderFactory
|
||||
) {}
|
||||
|
||||
@ResolveField(() => CopilotQuotaType, {
|
||||
@@ -724,6 +731,65 @@ export class CopilotResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => String, {
|
||||
description:
|
||||
'Apply updates to a doc using LLM and return the merged markdown.',
|
||||
})
|
||||
async applyDocUpdates(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args({ name: 'workspaceId', type: () => String })
|
||||
workspaceId: string,
|
||||
@Args({ name: 'docId', type: () => String })
|
||||
docId: string,
|
||||
@Args({ name: 'op', type: () => String })
|
||||
op: string,
|
||||
@Args({ name: 'updates', type: () => String })
|
||||
updates: string
|
||||
): Promise<string> {
|
||||
await this.assertPermission(user, { workspaceId, docId });
|
||||
|
||||
const docContent = await this.docReader.getDocMarkdown(
|
||||
workspaceId,
|
||||
docId,
|
||||
true
|
||||
);
|
||||
if (!docContent || !docContent.markdown) {
|
||||
throw new NotFoundException('Doc not found or empty');
|
||||
}
|
||||
|
||||
const markdown = docContent.markdown.trim();
|
||||
|
||||
// Get LLM provider
|
||||
const provider =
|
||||
await this.providerFactory.getProviderByModel('morph-v3-large');
|
||||
if (!provider) {
|
||||
throw new BadRequestException('No LLM provider available');
|
||||
}
|
||||
|
||||
try {
|
||||
return await provider.text(
|
||||
{ modelId: 'morph-v3-large' },
|
||||
[
|
||||
{
|
||||
role: 'user',
|
||||
content: `<instruction>${op}</instruction>\n<code>${markdown}</code>\n<update>${updates}</update>`,
|
||||
},
|
||||
],
|
||||
{ reasoning: false }
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
} else {
|
||||
throw new CopilotProviderSideError({
|
||||
provider: provider.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected apply response',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private transformToSessionType(
|
||||
session: Omit<ChatHistory, 'messages'>
|
||||
): CopilotSessionType {
|
||||
@@ -773,7 +839,18 @@ class CreateCopilotPromptInput {
|
||||
@Admin()
|
||||
@Resolver(() => String)
|
||||
export class PromptsManagementResolver {
|
||||
constructor(private readonly promptService: PromptService) {}
|
||||
constructor(
|
||||
private readonly cron: CopilotCronJobs,
|
||||
private readonly promptService: PromptService
|
||||
) {}
|
||||
|
||||
@Query(() => Boolean, {
|
||||
description: 'Trigger generate missing titles cron job',
|
||||
})
|
||||
async triggerGenerateTitleCron() {
|
||||
await this.cron.triggerGenerateMissingTitles();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Query(() => [CopilotPromptType], {
|
||||
description: 'List all copilot prompts',
|
||||
|
||||
@@ -507,6 +507,8 @@ export class ChatSessionService {
|
||||
return await this.models.copilotSession.fork({
|
||||
...session,
|
||||
userId: options.userId,
|
||||
// docId can be changed in fork
|
||||
docId: options.docId,
|
||||
sessionId: randomUUID(),
|
||||
parentSessionId: options.sessionId,
|
||||
messages,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
|
||||
import { DocReader } from '../../../core/doc';
|
||||
import { AccessController } from '../../../core/permission';
|
||||
import { type PromptService } from '../prompt';
|
||||
import type { CopilotChatOptions, CopilotProviderFactory } from '../providers';
|
||||
|
||||
export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
|
||||
@@ -24,14 +25,20 @@ export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
|
||||
|
||||
export const createDocEditTool = (
|
||||
factory: CopilotProviderFactory,
|
||||
prompt: PromptService,
|
||||
getContent: (targetId?: string) => Promise<string | undefined>
|
||||
) => {
|
||||
return tool({
|
||||
description: `
|
||||
Use this tool to propose an edit to a structured Markdown document with identifiable blocks. Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
|
||||
Use this tool to propose an edit to a structured Markdown document with identifiable blocks.
|
||||
Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
|
||||
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
|
||||
|
||||
Your task is to return a list of block-level changes needed to fulfill the user's intent. Each change should correspond to a specific user instruction and be represented by one of the following operations:
|
||||
If you receive a markdown without block_id comments, you should call \`doc_read\` tool to get the content.
|
||||
|
||||
Your task is to return a list of block-level changes needed to fulfill the user's intent. **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
|
||||
|
||||
Each change should correspond to a specific user instruction and be represented by one of the following operations:
|
||||
|
||||
replace: Replace the content of a block with updated Markdown.
|
||||
|
||||
@@ -41,83 +48,75 @@ insert: Add a new block, and specify its block_id and content.
|
||||
|
||||
Important Instructions:
|
||||
- Use the existing block structure as-is. Do not reformat or reorder blocks unless explicitly asked.
|
||||
- Always preserve block_id and type in your replacements.
|
||||
- When replacing a block, use the full new block including <!-- block_id=... type=... --> and the updated content.
|
||||
- When inserting, follow the same format as a replacement, but ensure the new block_id does not conflict with existing IDs.
|
||||
- When replacing content, always keep the original block_id unchanged.
|
||||
- When deleting content, only use the format <!-- delete block_id=xxx -->, and only for valid block_id present in the original <code> content.
|
||||
- Each list item should be a block.
|
||||
- Use <!-- existing blocks ... --> for unchanged sections.
|
||||
- If you plan on deleting a section, you must provide surrounding context to indicate the deletion.
|
||||
- Your task is to return a list of block-level changes needed to fulfill the user's intent.
|
||||
- **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
|
||||
|
||||
Example Input Document:
|
||||
\`\`\`md
|
||||
<!-- block_id=block-001 type=paragraph -->
|
||||
# My Holiday Plan
|
||||
Original Content:
|
||||
\`\`\`markdown
|
||||
<!-- block_id=001 flavour=paragraph -->
|
||||
# Andriy Shevchenko
|
||||
|
||||
<!-- block_id=block-002 type=paragraph -->
|
||||
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
|
||||
<!-- block_id=002 flavour=paragraph -->
|
||||
## Player Profile
|
||||
|
||||
<!-- block_id=block-003 type=paragraph -->
|
||||
I love Paris.
|
||||
<!-- block_id=003 flavour=paragraph -->
|
||||
Andriy Shevchenko is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
|
||||
|
||||
<!-- block_id=block-004 type=paragraph -->
|
||||
## Reason for the delay
|
||||
<!-- block_id=004 flavour=paragraph -->
|
||||
## Career Overview
|
||||
|
||||
<!-- block_id=block-005 type=paragraph -->
|
||||
This plan has been brewing for a long time, but I always postponed it because I was too busy with work.
|
||||
|
||||
<!-- block_id=block-006 type=paragraph -->
|
||||
## Trip Steps
|
||||
|
||||
<!-- block_id=block-007 type=list -->
|
||||
- Book flight tickets
|
||||
<!-- block_id=block-008 type=list -->
|
||||
- Reserve a hotel
|
||||
<!-- block_id=block-009 type=list -->
|
||||
- Prepare visa documents
|
||||
<!-- block_id=block-010 type=list -->
|
||||
- Plan the itinerary
|
||||
|
||||
<!-- block_id=block-011 type=paragraph -->
|
||||
Additionally, I plan to learn some basic French to make communication easier during the trip.
|
||||
<!-- block_id=005 flavour=list -->
|
||||
- Born in 1976 in Ukraine.
|
||||
<!-- block_id=006 flavour=list -->
|
||||
- Rose to fame at Dynamo Kyiv in the 1990s.
|
||||
<!-- block_id=007 flavour=list -->
|
||||
- Starred at AC Milan (1999–2006), scoring over 170 goals.
|
||||
<!-- block_id=008 flavour=list -->
|
||||
- Played for Chelsea (2006–2009) before returning to Kyiv.
|
||||
<!-- block_id=009 flavour=list -->
|
||||
- Coached Ukraine national team, reaching Euro 2020 quarter-finals.
|
||||
\`\`\`
|
||||
|
||||
Example User Request:
|
||||
|
||||
User Request:
|
||||
\`\`\`
|
||||
Translate the trip steps to Chinese, remove the reason for the delay, and bold the final paragraph.
|
||||
Bold the player’s name in the intro, add a summary section at the end, and remove the career overview.
|
||||
\`\`\`
|
||||
|
||||
Expected Output:
|
||||
|
||||
\`\`\`md
|
||||
<!-- existing blocks ... -->
|
||||
|
||||
<!-- block_id=block-002 type=paragraph -->
|
||||
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
|
||||
|
||||
<!-- block_id=block-003 type=paragraph -->
|
||||
I love Paris.
|
||||
|
||||
<!-- delete block-004 -->
|
||||
|
||||
<!-- delete block-005 -->
|
||||
|
||||
<!-- block_id=block-006 type=paragraph -->
|
||||
## Trip Steps
|
||||
|
||||
<!-- block_id=block-007 type=list -->
|
||||
- 订机票
|
||||
<!-- block_id=block-008 type=list -->
|
||||
- 预定酒店
|
||||
<!-- block_id=block-009 type=list -->
|
||||
- 准备签证材料
|
||||
<!-- block_id=block-010 type=list -->
|
||||
- 规划行程
|
||||
|
||||
<!-- existing blocks ... -->
|
||||
|
||||
<!-- block_id=block-011 type=paragraph -->
|
||||
**Additionally, I plan to learn some basic French to make communication easier during the trip.**
|
||||
Example response:
|
||||
\`\`\`json
|
||||
[
|
||||
{
|
||||
"op": "Bold the player's name in the introduction",
|
||||
"updates": "
|
||||
<!-- block_id=003 flavour=paragraph -->
|
||||
**Andriy Shevchenko** is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
|
||||
"
|
||||
},
|
||||
{
|
||||
"op": "Add a summary section at the end",
|
||||
"updates": "
|
||||
<!-- block_id=new-abc123 flavour=paragraph -->
|
||||
## Summary
|
||||
<!-- block_id=new-def456 flavour=paragraph -->
|
||||
Shevchenko is celebrated as one of the greatest Ukrainian footballers of all time. Known for his composure, strength, and goal-scoring instinct, he left a lasting legacy both on and off the pitch.
|
||||
"
|
||||
},
|
||||
{
|
||||
"op": "Delete the career overview section",
|
||||
"updates": "
|
||||
<!-- delete block_id=004 -->
|
||||
<!-- delete block_id=005 -->
|
||||
<!-- delete block_id=006 -->
|
||||
<!-- delete block_id=007 -->
|
||||
<!-- delete block_id=008 -->
|
||||
<!-- delete block_id=009 -->
|
||||
"
|
||||
}
|
||||
]
|
||||
\`\`\`
|
||||
You should specify the following arguments before the others: [doc_id], [origin_content]
|
||||
|
||||
@@ -144,14 +143,32 @@ You should specify the following arguments before the others: [doc_id], [origin_
|
||||
),
|
||||
|
||||
code_edit: z
|
||||
.string()
|
||||
.array(
|
||||
z.object({
|
||||
op: z
|
||||
.string()
|
||||
.describe(
|
||||
'A short description of the change, such as "Bold intro name"'
|
||||
),
|
||||
updates: z
|
||||
.string()
|
||||
.describe(
|
||||
'Markdown block fragments that represent the change, including the block_id and type'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'Specify only the necessary Markdown block-level changes. Return a list of inserted, replaced, or deleted blocks. Each block must start with its <!-- block_id=... type=... --> comment. Use <!-- existing blocks ... --> for unchanged sections.If you plan on deleting a section, you must provide surrounding context to indicate the deletion.'
|
||||
'An array of independent semantic changes to apply to the document.'
|
||||
),
|
||||
}),
|
||||
execute: async ({ doc_id, origin_content, code_edit }) => {
|
||||
try {
|
||||
const provider = await factory.getProviderByModel('morph-v2');
|
||||
const applyPrompt = await prompt.get('Apply Updates');
|
||||
if (!applyPrompt) {
|
||||
return 'Prompt not found';
|
||||
}
|
||||
const model = applyPrompt.model;
|
||||
const provider = await factory.getProviderByModel(model);
|
||||
if (!provider) {
|
||||
return 'Editing docs is not supported';
|
||||
}
|
||||
@@ -160,14 +177,27 @@ You should specify the following arguments before the others: [doc_id], [origin_
|
||||
if (!content) {
|
||||
return 'Doc not found or doc is empty';
|
||||
}
|
||||
const result = await provider.text({ modelId: 'morph-v2' }, [
|
||||
{
|
||||
role: 'user',
|
||||
content: `<code>${content}</code>\n<update>${code_edit}</update>`,
|
||||
},
|
||||
]);
|
||||
|
||||
return { result, content };
|
||||
const changedContents = await Promise.all(
|
||||
code_edit.map(async edit => {
|
||||
return await provider.text({ modelId: model }, [
|
||||
...applyPrompt.finish({
|
||||
content,
|
||||
op: edit.op,
|
||||
updates: edit.updates,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
result: changedContents.map((changedContent, index) => ({
|
||||
op: code_edit[index].op,
|
||||
updates: code_edit[index].updates,
|
||||
originalContent: content,
|
||||
changedContent,
|
||||
})),
|
||||
};
|
||||
} catch {
|
||||
return 'Failed to apply edit to the doc';
|
||||
}
|
||||
|
||||
@@ -1518,6 +1518,9 @@ type PublicUserType {
|
||||
type Query {
|
||||
"""get the whole app configuration"""
|
||||
appConfig: JSONObject!
|
||||
|
||||
"""Apply updates to a doc using LLM and return the merged markdown."""
|
||||
applyDocUpdates(docId: String!, op: String!, updates: String!, workspaceId: String!): String!
|
||||
collectAllBlobSizes: WorkspaceBlobSizes! @deprecated(reason: "use `user.quotaUsage` instead")
|
||||
|
||||
"""Get current user"""
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
|
||||
applyDocUpdates(workspaceId: $workspaceId, docId: $docId, op: $op, updates: $updates)
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
query getCopilotRecentSessions(
|
||||
$workspaceId: String!
|
||||
$limit: Int = 10
|
||||
$offset: Int = 0
|
||||
) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
chats(
|
||||
pagination: { first: $limit }
|
||||
pagination: { first: $limit, offset: $offset }
|
||||
options: {
|
||||
action: false
|
||||
fork: false
|
||||
sessionOrder: desc
|
||||
withMessages: false
|
||||
|
||||
@@ -555,6 +555,19 @@ export const uploadCommentAttachmentMutation = {
|
||||
file: true,
|
||||
};
|
||||
|
||||
export const applyDocUpdatesQuery = {
|
||||
id: 'applyDocUpdatesQuery' as const,
|
||||
op: 'applyDocUpdates',
|
||||
query: `query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
|
||||
applyDocUpdates(
|
||||
workspaceId: $workspaceId
|
||||
docId: $docId
|
||||
op: $op
|
||||
updates: $updates
|
||||
)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const addContextCategoryMutation = {
|
||||
id: 'addContextCategoryMutation' as const,
|
||||
op: 'addContextCategory',
|
||||
@@ -1068,12 +1081,12 @@ ${paginatedCopilotChatsFragment}`,
|
||||
export const getCopilotRecentSessionsQuery = {
|
||||
id: 'getCopilotRecentSessionsQuery' as const,
|
||||
op: 'getCopilotRecentSessions',
|
||||
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) {
|
||||
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10, $offset: Int = 0) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
chats(
|
||||
pagination: {first: $limit}
|
||||
options: {fork: false, sessionOrder: desc, withMessages: false}
|
||||
pagination: {first: $limit, offset: $offset}
|
||||
options: {action: false, fork: false, sessionOrder: desc, withMessages: false}
|
||||
) {
|
||||
...PaginatedCopilotChats
|
||||
}
|
||||
|
||||
@@ -2073,6 +2073,8 @@ export interface Query {
|
||||
__typename?: 'Query';
|
||||
/** get the whole app configuration */
|
||||
appConfig: Scalars['JSONObject']['output'];
|
||||
/** Apply updates to a doc using LLM and return the merged markdown. */
|
||||
applyDocUpdates: Scalars['String']['output'];
|
||||
/** @deprecated use `user.quotaUsage` instead */
|
||||
collectAllBlobSizes: WorkspaceBlobSizes;
|
||||
/** Get current user */
|
||||
@@ -2120,6 +2122,13 @@ export interface Query {
|
||||
workspaces: Array<WorkspaceType>;
|
||||
}
|
||||
|
||||
export interface QueryApplyDocUpdatesArgs {
|
||||
docId: Scalars['String']['input'];
|
||||
op: Scalars['String']['input'];
|
||||
updates: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
export interface QueryErrorArgs {
|
||||
name: ErrorNames;
|
||||
}
|
||||
@@ -3509,6 +3518,18 @@ export type UploadCommentAttachmentMutation = {
|
||||
uploadCommentAttachment: string;
|
||||
};
|
||||
|
||||
export type ApplyDocUpdatesQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
docId: Scalars['String']['input'];
|
||||
op: Scalars['String']['input'];
|
||||
updates: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type ApplyDocUpdatesQuery = {
|
||||
__typename?: 'Query';
|
||||
applyDocUpdates: string;
|
||||
};
|
||||
|
||||
export type AddContextCategoryMutationVariables = Exact<{
|
||||
options: AddContextCategoryInput;
|
||||
}>;
|
||||
@@ -4365,6 +4386,7 @@ export type GetCopilotSessionQuery = {
|
||||
export type GetCopilotRecentSessionsQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
offset?: InputMaybe<Scalars['Int']['input']>;
|
||||
}>;
|
||||
|
||||
export type GetCopilotRecentSessionsQuery = {
|
||||
@@ -6147,6 +6169,11 @@ export type Queries =
|
||||
variables: ListCommentsQueryVariables;
|
||||
response: ListCommentsQuery;
|
||||
}
|
||||
| {
|
||||
name: 'applyDocUpdatesQuery';
|
||||
variables: ApplyDocUpdatesQueryVariables;
|
||||
response: ApplyDocUpdatesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'listContextObjectQuery';
|
||||
variables: ListContextObjectQueryVariables;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
:root {
|
||||
--noise-background: url(./noise.avif);
|
||||
text-autospace: normal;
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
@@ -348,6 +348,12 @@ declare global {
|
||||
files?: ContextMatchedFileChunk[];
|
||||
docs?: ContextMatchedDocChunk[];
|
||||
}>;
|
||||
applyDocUpdates: (
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
op: string,
|
||||
updates: string
|
||||
) => Promise<string>;
|
||||
}
|
||||
|
||||
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
EdgelessEditorActions,
|
||||
PageEditorActions,
|
||||
} from '../../_common/chat-actions-handle';
|
||||
import type { DocDisplayConfig } from '../../components/ai-chat-chips';
|
||||
import {
|
||||
type ChatMessage,
|
||||
type ChatStatus,
|
||||
@@ -79,6 +80,12 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
get state() {
|
||||
const { isLast, status } = this;
|
||||
return isLast
|
||||
@@ -138,6 +145,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.affineThemeService.appTheme.themeSignal}
|
||||
.docDisplayService=${this.docDisplayService}
|
||||
></chat-content-stream-objects>`;
|
||||
}
|
||||
|
||||
@@ -175,7 +183,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
: EdgelessEditorActions
|
||||
: null;
|
||||
|
||||
const showActions = host && !!markdown;
|
||||
const showActions = host && !!markdown && !this.independentMode;
|
||||
|
||||
return html`
|
||||
<chat-copy-more
|
||||
|
||||
@@ -23,6 +23,12 @@ export class AIChatAddContext extends SignalWatcher(
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docId: string | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
||||
|
||||
@@ -69,6 +75,8 @@ export class AIChatAddContext extends SignalWatcher(
|
||||
createLitPortal({
|
||||
template: html`
|
||||
<chat-panel-add-popover
|
||||
.docId=${this.docId}
|
||||
.independentMode=${this.independentMode}
|
||||
.addChip=${this.addChip}
|
||||
.addImages=${this.addImages}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from '@affine/component';
|
||||
import type { TagMeta } from '@affine/core/components/page-list';
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import track from '@affine/track';
|
||||
import track, { type EventArgs } from '@affine/track';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -120,6 +120,12 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
|
||||
private accessor _query = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docId: string | undefined;
|
||||
|
||||
@state()
|
||||
private accessor _searchGroups: MenuGroup[] = [];
|
||||
|
||||
@@ -497,7 +503,8 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
state: 'processing',
|
||||
});
|
||||
const mode = this.docDisplayConfig.getDocPrimaryMode(meta.id);
|
||||
this._track('doc', mode);
|
||||
const method = meta.id === this.docId ? 'cur-doc' : 'doc';
|
||||
this._track(method, mode);
|
||||
};
|
||||
|
||||
private readonly _addTagChip = async (tag: TagMeta) => {
|
||||
@@ -561,10 +568,13 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
}
|
||||
|
||||
private _track(
|
||||
method: 'doc' | 'file' | 'tags' | 'collections',
|
||||
method: EventArgs['addEmbeddingDoc']['method'],
|
||||
type?: 'page' | 'edgeless'
|
||||
) {
|
||||
track.$.chatPanel.chatPanelInput.addEmbeddingDoc({
|
||||
const page = this.independentMode
|
||||
? track.$.intelligence
|
||||
: track.$.chatPanel;
|
||||
page.chatPanelInput.addEmbeddingDoc({
|
||||
control: 'addButton',
|
||||
method,
|
||||
type,
|
||||
|
||||
@@ -81,6 +81,9 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor isCollapsed!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
||||
|
||||
@@ -142,6 +145,7 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
if (isDocChip(chip)) {
|
||||
return html`<chat-panel-doc-chip
|
||||
.chip=${chip}
|
||||
.independentMode=${this.independentMode}
|
||||
.addChip=${this.addChip}
|
||||
.updateChip=${this.updateChip}
|
||||
.removeChip=${this.removeChip}
|
||||
|
||||
@@ -18,6 +18,9 @@ export class ChatPanelDocChip extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor chip!: DocChip;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChip!: (chip: DocChip) => void;
|
||||
|
||||
@@ -81,7 +84,10 @@ export class ChatPanelDocChip extends SignalWatcher(
|
||||
state: 'processing',
|
||||
});
|
||||
const mode = this.docDisplayConfig.getDocPrimaryMode(this.chip.docId);
|
||||
track.$.chatPanel.chatPanelInput.addEmbeddingDoc({
|
||||
const page = this.independentMode
|
||||
? track.$.intelligence
|
||||
: track.$.chatPanel;
|
||||
page.chatPanelInput.addEmbeddingDoc({
|
||||
control: 'addButton',
|
||||
method: 'suggestion',
|
||||
type: mode,
|
||||
|
||||
@@ -59,7 +59,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode!: boolean;
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host: EditorHost | null | undefined;
|
||||
@@ -85,9 +85,9 @@ export class AIChatComposer extends SignalWatcher(
|
||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmbeddingProgressChange!: (
|
||||
count: Record<ContextEmbedStatus, number>
|
||||
) => void;
|
||||
accessor onEmbeddingProgressChange:
|
||||
| ((count: Record<ContextEmbedStatus, number>) => void)
|
||||
| undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
@@ -136,6 +136,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
<chat-panel-chips
|
||||
.chips=${this.chips}
|
||||
.isCollapsed=${this.isChipsCollapsed}
|
||||
.independentMode=${this.independentMode}
|
||||
.addChip=${this.addChip}
|
||||
.updateChip=${this.updateChip}
|
||||
.removeChip=${this.removeChip}
|
||||
@@ -603,7 +604,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
return chip;
|
||||
});
|
||||
this.updateChips(nextChips);
|
||||
this.onEmbeddingProgressChange(count);
|
||||
this.onEmbeddingProgressChange?.(count);
|
||||
if (count.processing === 0) {
|
||||
this._abortPoll();
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode!: boolean;
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onboardingOffsetY!: number;
|
||||
@@ -177,9 +177,9 @@ export class AIChatContent extends SignalWatcher(
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmbeddingProgressChange!: (
|
||||
count: Record<ContextEmbedStatus, number>
|
||||
) => void;
|
||||
accessor onEmbeddingProgressChange:
|
||||
| ((count: Record<ContextEmbedStatus, number>) => void)
|
||||
| undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onContextChange!: (context: Partial<ChatContextValue>) => void;
|
||||
@@ -219,14 +219,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
}
|
||||
|
||||
get showActions() {
|
||||
if (this.docId) {
|
||||
if (!this.session) {
|
||||
return true;
|
||||
}
|
||||
return this.session.docId === this.docId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly updateHistory = async () => {
|
||||
@@ -269,7 +262,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly updateActions = async () => {
|
||||
if (!this.docId || !AIProvider.histories) {
|
||||
if (!this.docId || !AIProvider.histories || !this.showActions) {
|
||||
return;
|
||||
}
|
||||
const actions = await AIProvider.histories.actions(
|
||||
@@ -403,7 +396,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
<ai-chat-messages
|
||||
class=${classMap({
|
||||
'ai-chat-messages': true,
|
||||
'independent-mode': this.independentMode,
|
||||
'independent-mode': !!this.independentMode,
|
||||
'no-message': this.messages.length === 0,
|
||||
})}
|
||||
${ref(this.chatMessagesRef)}
|
||||
@@ -424,6 +417,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.width=${this.width}
|
||||
.independentMode=${this.independentMode}
|
||||
.messages=${this.messages}
|
||||
.docDisplayService=${this.docDisplayConfig}
|
||||
></ai-chat-messages>
|
||||
<ai-chat-composer
|
||||
style=${styleMap({
|
||||
|
||||
@@ -290,7 +290,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode!: boolean;
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host: EditorHost | null | undefined;
|
||||
@@ -458,6 +458,8 @@ export class AIChatInput extends SignalWatcher(
|
||||
<div class="chat-panel-input-actions">
|
||||
<div class="chat-input-icon">
|
||||
<ai-chat-add-context
|
||||
.docId=${this.docId}
|
||||
.independentMode=${this.independentMode}
|
||||
.addChip=${this.addChip}
|
||||
.addImages=${this.addImages}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AffineIcon } from '../../_common/icons';
|
||||
import { AIPreloadConfig } from '../../chat-panel/preload-config';
|
||||
import { type AIError, AIProvider, UnauthorizedError } from '../../provider';
|
||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import { type ChatContextValue } from '../ai-chat-content/type';
|
||||
import type {
|
||||
AINetworkSearchConfig,
|
||||
@@ -150,7 +151,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
accessor avatarUrl = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode!: boolean;
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor messages!: HistoryMessage[];
|
||||
@@ -202,6 +203,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
@query('.chat-panel-messages-container')
|
||||
accessor messagesContainer: HTMLDivElement | null = null;
|
||||
|
||||
@@ -275,7 +279,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
<div
|
||||
class=${classMap({
|
||||
'chat-panel-messages-container': true,
|
||||
'independent-mode': this.independentMode,
|
||||
'independent-mode': !!this.independentMode,
|
||||
})}
|
||||
data-testid="chat-panel-messages-container"
|
||||
@scroll=${() => this._debouncedOnScroll()}
|
||||
@@ -327,6 +331,8 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
.notificationService=${this.notificationService}
|
||||
.retry=${() => this.retry()}
|
||||
.width=${this.width}
|
||||
.independentMode=${this.independentMode}
|
||||
.docDisplayService=${this.docDisplayService}
|
||||
></chat-message-assistant>`;
|
||||
} else if (isChatAction(item) && this.host) {
|
||||
return html`<chat-message-action
|
||||
|
||||
@@ -13,6 +13,7 @@ import { css, html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { AffineAIPanelState } from '../../widgets/ai-panel/type';
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import type { StreamObject } from '../ai-chat-messages';
|
||||
|
||||
export class ChatContentStreamObjects extends WithDisposable(
|
||||
@@ -54,6 +55,9 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
private renderToolCall(streamObject: StreamObject) {
|
||||
if (streamObject.type !== 'tool-call') {
|
||||
return nothing;
|
||||
@@ -101,6 +105,21 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.notificationService=${this.notificationService}
|
||||
></doc-edit-tool>
|
||||
`;
|
||||
case 'doc_semantic_search':
|
||||
return html`<doc-semantic-search-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-semantic-search-result>`;
|
||||
case 'doc_keyword_search':
|
||||
return html`<doc-keyword-search-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-keyword-search-result>`;
|
||||
case 'doc_read':
|
||||
return html`<doc-read-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool calling';
|
||||
return html`
|
||||
@@ -159,6 +178,22 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.notificationService=${this.notificationService}
|
||||
></doc-edit-tool>
|
||||
`;
|
||||
case 'doc_semantic_search':
|
||||
return html`<doc-semantic-search-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
.docDisplayService=${this.docDisplayService}
|
||||
></doc-semantic-search-result>`;
|
||||
case 'doc_keyword_search':
|
||||
return html`<doc-keyword-search-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-keyword-search-result>`;
|
||||
case 'doc_read':
|
||||
return html`<doc-read-result
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool result';
|
||||
return html`
|
||||
|
||||
@@ -34,7 +34,7 @@ export abstract class ArtifactTool<
|
||||
padding: 10px 0;
|
||||
|
||||
.affine-embed-linked-doc-block {
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
box-shadow: ${unsafeCSSVar('buttonShadow')};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export class ArtifactPreviewPanel extends WithDisposable(ShadowlessElement) {
|
||||
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artifact-panel-header {
|
||||
@@ -71,9 +71,6 @@ export class ArtifactPreviewPanel extends WithDisposable(ShadowlessElement) {
|
||||
justify-content: flex-end;
|
||||
padding: 0 12px;
|
||||
height: 52px;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
}
|
||||
|
||||
@@ -105,7 +102,9 @@ export class ArtifactPreviewPanel extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
.artifact-panel-content {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 52px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.artifact-panel-close:hover {
|
||||
|
||||
@@ -352,6 +352,8 @@ export class CodeArtifactTool extends ArtifactTool<
|
||||
> {
|
||||
static override styles = css`
|
||||
.code-artifact-preview {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -363,6 +365,11 @@ export class CodeArtifactTool extends ArtifactTool<
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-artifact-preview > code-highlighter {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-artifact-preview :is(.html-preview-iframe, .html-preview-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ export class DocComposeTool extends ArtifactTool<
|
||||
static override styles = css`
|
||||
.doc-compose-result-preview {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.doc-compose-result-preview-title {
|
||||
@@ -124,6 +123,7 @@ export class DocComposeTool extends ArtifactTool<
|
||||
.options=${{
|
||||
customHeading: true,
|
||||
extensions: getCustomPageEditorBlockSpecs(),
|
||||
theme: this.theme,
|
||||
}}
|
||||
></text-renderer>`
|
||||
: html`<div class="doc-compose-result-preview-loading">
|
||||
|
||||
@@ -2,6 +2,7 @@ import track from '@affine/track';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { AIStarIconWithAnimation } from '@blocksuite/affine-components/icons';
|
||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
CloseIcon,
|
||||
@@ -14,7 +15,9 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { AIProvider } from '../../provider';
|
||||
import { BlockDiffProvider } from '../../services/block-diff';
|
||||
import { diffMarkdown } from '../../utils/apply-model/markdown-diff';
|
||||
import { copyText } from '../../utils/editor-actions';
|
||||
@@ -37,8 +40,12 @@ interface DocEditToolResult {
|
||||
};
|
||||
result:
|
||||
| {
|
||||
result: string;
|
||||
content: string;
|
||||
result: {
|
||||
op: string;
|
||||
updates: string;
|
||||
originalContent: string;
|
||||
changedContent: string;
|
||||
}[];
|
||||
}
|
||||
| ToolError
|
||||
| null;
|
||||
@@ -199,40 +206,108 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
@state()
|
||||
accessor isCollapsed = false;
|
||||
|
||||
@state()
|
||||
accessor applyingMap: Record<string, boolean> = {};
|
||||
|
||||
@state()
|
||||
accessor acceptingMap: Record<string, boolean> = {};
|
||||
|
||||
get blockDiffService() {
|
||||
return this.host?.std.getOptional(BlockDiffProvider);
|
||||
}
|
||||
|
||||
private async _handleApply(markdown: string) {
|
||||
if (!this.host || this.data.type !== 'tool-result') {
|
||||
return;
|
||||
}
|
||||
track.applyModel.chat.$.apply({
|
||||
instruction: this.data.args.instructions,
|
||||
});
|
||||
await this.blockDiffService?.apply(this.host.store, markdown);
|
||||
get isBusy() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _handleReject(changedMarkdown: string) {
|
||||
isBusyForOp(op: string) {
|
||||
return this.applyingMap[op] || this.acceptingMap[op];
|
||||
}
|
||||
|
||||
private async _handleApply(op: string, updates: string) {
|
||||
if (
|
||||
!this.host ||
|
||||
this.data.type !== 'tool-result' ||
|
||||
this.isBusyForOp(op)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.applyingMap = { ...this.applyingMap, [op]: true };
|
||||
try {
|
||||
const markdown = await AIProvider.context?.applyDocUpdates(
|
||||
this.host.std.workspace.id,
|
||||
this.data.args.doc_id,
|
||||
op,
|
||||
updates
|
||||
);
|
||||
if (!markdown) {
|
||||
return;
|
||||
}
|
||||
track.applyModel.chat.$.apply({
|
||||
instruction: this.data.args.instructions,
|
||||
operation: op,
|
||||
});
|
||||
await this.blockDiffService?.apply(this.host.store, markdown);
|
||||
} catch (error) {
|
||||
this.notificationService.notify({
|
||||
title: 'Failed to apply updates',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
accent: 'error',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
} finally {
|
||||
this.applyingMap = { ...this.applyingMap, [op]: false };
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleReject(op: string) {
|
||||
if (!this.host || this.data.type !== 'tool-result') {
|
||||
return;
|
||||
}
|
||||
// TODO: set the rejected status
|
||||
track.applyModel.chat.$.reject({
|
||||
instruction: this.data.args.instructions,
|
||||
operation: op,
|
||||
});
|
||||
this.blockDiffService?.setChangedMarkdown(changedMarkdown);
|
||||
this.blockDiffService?.setChangedMarkdown(null);
|
||||
this.blockDiffService?.rejectAll();
|
||||
}
|
||||
|
||||
private async _handleAccept(changedMarkdown: string) {
|
||||
if (!this.host || this.data.type !== 'tool-result') {
|
||||
private async _handleAccept(op: string, updates: string) {
|
||||
if (
|
||||
!this.host ||
|
||||
this.data.type !== 'tool-result' ||
|
||||
this.isBusyForOp(op)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
track.applyModel.chat.$.accept({
|
||||
instruction: this.data.args.instructions,
|
||||
});
|
||||
await this.blockDiffService?.apply(this.host.store, changedMarkdown);
|
||||
await this.blockDiffService?.acceptAll(this.host.store);
|
||||
this.acceptingMap = { ...this.acceptingMap, [op]: true };
|
||||
try {
|
||||
const changedMarkdown = await AIProvider.context?.applyDocUpdates(
|
||||
this.host.std.workspace.id,
|
||||
this.data.args.doc_id,
|
||||
op,
|
||||
updates
|
||||
);
|
||||
if (!changedMarkdown) {
|
||||
return;
|
||||
}
|
||||
track.applyModel.chat.$.accept({
|
||||
instruction: this.data.args.instructions,
|
||||
operation: op,
|
||||
});
|
||||
await this.blockDiffService?.apply(this.host.store, changedMarkdown);
|
||||
await this.blockDiffService?.acceptAll(this.host.store);
|
||||
} catch (error) {
|
||||
this.notificationService.notify({
|
||||
title: 'Failed to apply updates',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
accent: 'error',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
} finally {
|
||||
this.acceptingMap = { ...this.acceptingMap, [op]: false };
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleCollapse() {
|
||||
@@ -322,69 +397,84 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
const result = this.data.result;
|
||||
|
||||
if (result && 'result' in result && 'content' in result) {
|
||||
const { result: changedMarkdown, content } = result;
|
||||
const { instructions, doc_id: docId } = this.data.args;
|
||||
if (result && 'result' in result && Array.isArray(result.result)) {
|
||||
const { doc_id: docId } = this.data.args;
|
||||
|
||||
const diffs = diffMarkdown(content, changedMarkdown);
|
||||
|
||||
return html`
|
||||
<div class="doc-edit-tool-result-wrapper">
|
||||
<div class="doc-edit-tool-result-title">${instructions}</div>
|
||||
<div
|
||||
class="doc-edit-tool-result-card ${this.isCollapsed
|
||||
? 'collapsed'
|
||||
: ''}"
|
||||
>
|
||||
<div class="doc-edit-tool-result-card-header">
|
||||
<div class="doc-edit-tool-result-card-header-title">
|
||||
${PenIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
${docId}
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-header-operations">
|
||||
<span @click=${() => this._toggleCollapse()}
|
||||
>${this.isCollapsed
|
||||
? ExpandFullIcon()
|
||||
: ExpandCloseIcon()}</span
|
||||
>
|
||||
<span @click=${() => this._handleCopy(changedMarkdown)}>
|
||||
${CopyIcon()}
|
||||
</span>
|
||||
<button @click=${() => this._handleApply(changedMarkdown)}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-content">
|
||||
<div class="doc-edit-tool-result-card-content-title">
|
||||
${this.renderBlockDiffs(diffs)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-footer">
|
||||
return repeat(
|
||||
result.result,
|
||||
change => change.op,
|
||||
({ op, updates, originalContent, changedContent }) => {
|
||||
const diffs = diffMarkdown(originalContent, changedContent);
|
||||
return html`
|
||||
<div class="doc-edit-tool-result-wrapper">
|
||||
<div class="doc-edit-tool-result-title">${op}</div>
|
||||
<div
|
||||
class="doc-edit-tool-result-reject"
|
||||
@click=${() => this._handleReject(changedMarkdown)}
|
||||
class="doc-edit-tool-result-card ${this.isCollapsed
|
||||
? 'collapsed'
|
||||
: ''}"
|
||||
>
|
||||
${CloseIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
|
||||
})}
|
||||
Reject
|
||||
</div>
|
||||
<div
|
||||
class="doc-edit-tool-result-accept"
|
||||
@click=${() => this._handleAccept(changedMarkdown)}
|
||||
>
|
||||
${DoneIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
Accept
|
||||
<div class="doc-edit-tool-result-card-header">
|
||||
<div class="doc-edit-tool-result-card-header-title">
|
||||
${PenIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
${docId}
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-header-operations">
|
||||
<span @click=${() => this._toggleCollapse()}
|
||||
>${this.isCollapsed
|
||||
? ExpandFullIcon()
|
||||
: ExpandCloseIcon()}</span
|
||||
>
|
||||
<span @click=${() => this._handleCopy(changedContent)}>
|
||||
${CopyIcon()}
|
||||
</span>
|
||||
<button
|
||||
@click=${() => this._handleApply(op, updates)}
|
||||
?disabled=${this.isBusyForOp(op)}
|
||||
>
|
||||
${this.applyingMap[op]
|
||||
? AIStarIconWithAnimation
|
||||
: html`Apply`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-content">
|
||||
<div class="doc-edit-tool-result-card-content-title">
|
||||
${this.renderBlockDiffs(diffs)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-footer">
|
||||
<div
|
||||
class="doc-edit-tool-result-reject"
|
||||
@click=${() => this._handleReject(op)}
|
||||
>
|
||||
${CloseIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
|
||||
})}
|
||||
Reject
|
||||
</div>
|
||||
<button
|
||||
class="doc-edit-tool-result-accept"
|
||||
@click=${() => this._handleAccept(op, updates)}
|
||||
?disabled=${this.isBusyForOp(op)}
|
||||
style="${this.isBusyForOp(op)
|
||||
? 'pointer-events: none; opacity: 0.6;'
|
||||
: ''}"
|
||||
>
|
||||
${this.acceptingMap[op]
|
||||
? AIStarIconWithAnimation
|
||||
: DoneIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
${this.acceptingMap[op] ? 'Accepting...' : 'Accept'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { ToolResult } from './tool-result-card';
|
||||
|
||||
interface DocKeywordSearchToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
}
|
||||
|
||||
interface DocKeywordSearchToolResult {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
result: Array<{
|
||||
title: string;
|
||||
docId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor data!: DocKeywordSearchToolCall | DocKeywordSearchToolResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
renderToolCall() {
|
||||
return html`<tool-call-card
|
||||
.name=${`Searching workspace documents for "${this.data.args.query}"`}
|
||||
.icon=${SearchIcon()}
|
||||
.width=${this.width}
|
||||
></tool-call-card>`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
let results: ToolResult[] = [];
|
||||
try {
|
||||
results = this.data.result.map(item => ({
|
||||
title: item.title,
|
||||
icon: PageIcon(),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Failed to parse result', err);
|
||||
}
|
||||
return html`<tool-result-card
|
||||
.name=${`Found ${this.data.result.length} pages for "${this.data.args.query}"`}
|
||||
.icon=${SearchIcon()}
|
||||
.width=${this.width}
|
||||
.results=${results}
|
||||
></tool-result-card>`;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
if (this.data.type === 'tool-call') {
|
||||
return this.renderToolCall();
|
||||
}
|
||||
return this.renderToolResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { PageIcon, ViewIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
interface DocReadToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { doc_id: string };
|
||||
}
|
||||
|
||||
interface DocReadToolResult {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { doc_id: string };
|
||||
result: {
|
||||
title: string;
|
||||
markdown: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class DocReadResult extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor data!: DocReadToolCall | DocReadToolResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
renderToolCall() {
|
||||
// TODO: get document name by doc_id
|
||||
return html`<tool-call-card
|
||||
.name=${`Reading document`}
|
||||
.icon=${ViewIcon()}
|
||||
.width=${this.width}
|
||||
></tool-call-card>`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
// TODO: better markdown rendering
|
||||
return html`<tool-result-card
|
||||
.name=${`Read "${this.data.result.title}"`}
|
||||
.icon=${ViewIcon()}
|
||||
.width=${this.width}
|
||||
.results=${[
|
||||
{
|
||||
title: this.data.result.title,
|
||||
icon: PageIcon(),
|
||||
content: this.data.result.markdown,
|
||||
},
|
||||
]}
|
||||
></tool-result-card>`;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
if (this.data.type === 'tool-call') {
|
||||
return this.renderToolCall();
|
||||
}
|
||||
if (this.data.type === 'tool-result') {
|
||||
return this.renderToolResult();
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
|
||||
interface DocSemanticSearchToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
}
|
||||
|
||||
interface DocSemanticSearchToolResult {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { query: string };
|
||||
result: Array<{
|
||||
content: string;
|
||||
docId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function parseResultContent(content: string) {
|
||||
const properties = [
|
||||
'Title',
|
||||
'Created at',
|
||||
'Updated at',
|
||||
'Created by',
|
||||
'Updated by',
|
||||
];
|
||||
try {
|
||||
// A row starts with "Title: ${title}\n"
|
||||
const title = content.match(/^Title:\s+(.*)\n/)?.[1];
|
||||
// from first row that not starts with "${propertyName}:" to end of the content
|
||||
const rows = content.split('\n');
|
||||
const startIndex = rows.findIndex(
|
||||
line => !properties.some(property => line.startsWith(`${property}:`))
|
||||
);
|
||||
const text = rows.slice(startIndex).join('\n');
|
||||
return {
|
||||
title,
|
||||
content: text,
|
||||
icon: PageIcon(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to parse result content', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor data!: DocSemanticSearchToolCall | DocSemanticSearchToolResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
renderToolCall() {
|
||||
return html`<tool-call-card
|
||||
.name=${`Finding semantically related pages for "${this.data.args.query}"`}
|
||||
.icon=${AiEmbeddingIcon()}
|
||||
.width=${this.width}
|
||||
></tool-call-card>`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
return html`<tool-result-card
|
||||
.name=${`Found semantically related pages for "${this.data.args.query}"`}
|
||||
.icon=${AiEmbeddingIcon()}
|
||||
.width=${this.width}
|
||||
.results=${this.data.result
|
||||
.map(result => ({
|
||||
...parseResultContent(result.content),
|
||||
title: this.docDisplayService.getTitle(result.docId),
|
||||
}))
|
||||
.filter(Boolean)}
|
||||
></tool-result-card>`;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const { data } = this;
|
||||
|
||||
if (data.type === 'tool-call') {
|
||||
return this.renderToolCall();
|
||||
}
|
||||
if (data.type === 'tool-result') {
|
||||
return this.renderToolResult();
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'doc-semantic-search-result': DocSemanticSearchResult;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { type Signal } from '@preact/signals-core';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
interface ToolResult {
|
||||
export interface ToolResult {
|
||||
title: string;
|
||||
icon?: string | TemplateResult<1>;
|
||||
content?: string;
|
||||
|
||||
@@ -160,6 +160,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
||||
mainAxis: 0,
|
||||
crossAxis: -100,
|
||||
});
|
||||
this.disposables.add(() => this._morePopper?.dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ import {
|
||||
} from './components/ai-tools/code-artifact';
|
||||
import { DocComposeTool } from './components/ai-tools/doc-compose';
|
||||
import { DocEditTool } from './components/ai-tools/doc-edit';
|
||||
import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
|
||||
import { DocReadResult } from './components/ai-tools/doc-read-result';
|
||||
import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
|
||||
import { ToolCallCard } from './components/ai-tools/tool-call-card';
|
||||
import { ToolFailedCard } from './components/ai-tools/tool-failed-card';
|
||||
import { ToolResultCard } from './components/ai-tools/tool-result-card';
|
||||
@@ -208,6 +211,9 @@ export function registerAIEffects() {
|
||||
customElements.define('tool-call-card', ToolCallCard);
|
||||
customElements.define('tool-result-card', ToolResultCard);
|
||||
customElements.define('tool-call-failed', ToolFailedCard);
|
||||
customElements.define('doc-semantic-search-result', DocSemanticSearchResult);
|
||||
customElements.define('doc-keyword-search-result', DocKeywordSearchResult);
|
||||
customElements.define('doc-read-result', DocReadResult);
|
||||
customElements.define('web-crawl-tool', WebCrawlTool);
|
||||
customElements.define('web-search-tool', WebSearchTool);
|
||||
customElements.define('doc-compose-tool', DocComposeTool);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
addContextCategoryMutation,
|
||||
addContextDocMutation,
|
||||
addContextFileMutation,
|
||||
applyDocUpdatesQuery,
|
||||
cleanupCopilotSessionMutation,
|
||||
createCopilotContextMutation,
|
||||
createCopilotMessageMutation,
|
||||
@@ -500,4 +501,21 @@ export class CopilotClient {
|
||||
variables: { workspaceId },
|
||||
}).then(res => res.queryWorkspaceEmbeddingStatus);
|
||||
}
|
||||
|
||||
applyDocUpdates(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
op: string,
|
||||
updates: string
|
||||
) {
|
||||
return this.gql({
|
||||
query: applyDocUpdatesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
docId,
|
||||
op,
|
||||
updates,
|
||||
},
|
||||
}).then(res => res.applyDocUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +733,14 @@ Could you make a new website based on these notes and send back just the html fi
|
||||
threshold
|
||||
);
|
||||
},
|
||||
applyDocUpdates: async (
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
op: string,
|
||||
updates: string
|
||||
) => {
|
||||
return client.applyDocUpdates(workspaceId, docId, op, updates);
|
||||
},
|
||||
});
|
||||
|
||||
AIProvider.provide('histories', {
|
||||
|
||||
@@ -80,13 +80,13 @@ export interface BlockDiffProvider {
|
||||
* Set the original markdown
|
||||
* @param originalMarkdown - The original markdown
|
||||
*/
|
||||
setOriginalMarkdown(originalMarkdown: string): void;
|
||||
setOriginalMarkdown(originalMarkdown: string | null): void;
|
||||
|
||||
/**
|
||||
* Set the changed markdown
|
||||
* @param changedMarkdown - The changed markdown
|
||||
*/
|
||||
setChangedMarkdown(changedMarkdown: string): void;
|
||||
setChangedMarkdown(changedMarkdown: string | null): void;
|
||||
|
||||
/**
|
||||
* Apply the diff to the doc
|
||||
|
||||
@@ -54,6 +54,8 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
||||
|
||||
private _selectionModelRect!: DOMRect;
|
||||
|
||||
private _autoUpdateCleanup: (() => void) | null = null;
|
||||
|
||||
groups: AIItemGroupConfig[] = [];
|
||||
|
||||
get gfx() {
|
||||
@@ -145,7 +147,8 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
||||
|
||||
const originMaxHeight = window.getComputedStyle(panel).maxHeight;
|
||||
|
||||
autoUpdate(referenceElement, panel, () => {
|
||||
this._autoUpdateCleanup?.();
|
||||
this._autoUpdateCleanup = autoUpdate(referenceElement, panel, () => {
|
||||
computePosition(referenceElement, panel, {
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
@@ -267,6 +270,8 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
||||
this._copilotPanel = null;
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(() => this._autoUpdateCleanup?.());
|
||||
}
|
||||
|
||||
determineInsertionBounds(width = 800, height = 95) {
|
||||
|
||||
@@ -42,6 +42,8 @@ export const empty = style({
|
||||
padding: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
lineHeight: '24px',
|
||||
justifyContent: 'center',
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@affine/component';
|
||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||
import { Guard } from '@affine/core/components/guard';
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
@@ -56,6 +57,7 @@ export const useNavigationPanelDocNodeOperations = (
|
||||
|
||||
const [addLinkedPageLoading, setAddLinkedPageLoading] = useState(false);
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const { createPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
@@ -149,20 +151,26 @@ export const useNavigationPanelDocNodeOperations = (
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
index: 0,
|
||||
inline: true,
|
||||
view: (
|
||||
<IconButton
|
||||
size="16"
|
||||
icon={<PlusIcon />}
|
||||
tooltip={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
|
||||
onClick={handleAddLinkedPage}
|
||||
loading={addLinkedPageLoading}
|
||||
disabled={addLinkedPageLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(appSettings.showLinkedDocInSidebar
|
||||
? [
|
||||
{
|
||||
index: 0,
|
||||
inline: true,
|
||||
view: (
|
||||
<IconButton
|
||||
size="16"
|
||||
icon={<PlusIcon />}
|
||||
tooltip={t[
|
||||
'com.affine.rootAppSidebar.explorer.doc-add-tooltip'
|
||||
]()}
|
||||
onClick={handleAddLinkedPage}
|
||||
loading={addLinkedPageLoading}
|
||||
disabled={addLinkedPageLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
index: 50,
|
||||
view: (
|
||||
@@ -258,6 +266,7 @@ export const useNavigationPanelDocNodeOperations = (
|
||||
],
|
||||
[
|
||||
addLinkedPageLoading,
|
||||
appSettings.showLinkedDocInSidebar,
|
||||
docId,
|
||||
favorite,
|
||||
handleAddLinkedPage,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
@@ -18,6 +18,7 @@ import * as styles from './style.css';
|
||||
export const DeleteAccount = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const serverService = useService(ServerService);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaceProfiles = workspacesService.getAllWorkspaceProfile();
|
||||
const isTeamWorkspaceOwner = workspaceProfiles.some(
|
||||
@@ -33,7 +34,9 @@ export const DeleteAccount = () => {
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: cssVarV2('status/error') }}>
|
||||
{t['com.affine.setting.account.delete']()}
|
||||
{t['com.affine.setting.account.delete-from-server']({
|
||||
server: serverService.server.config$.value.serverName,
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
desc={t['com.affine.setting.account.delete.message']()}
|
||||
@@ -96,6 +99,7 @@ const DeleteAccountModal = ({
|
||||
const authService = useService(AuthService);
|
||||
const session = authService.session;
|
||||
const account = useLiveData(session.account$);
|
||||
const serverService = useService(ServerService);
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -128,9 +132,22 @@ const DeleteAccountModal = ({
|
||||
onConfirm={onDeleteAccountConfirm}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t['com.affine.setting.account.delete.confirm-title']()}
|
||||
description={t[
|
||||
'com.affine.setting.account.delete.confirm-description-1'
|
||||
]()}
|
||||
description={
|
||||
<Trans
|
||||
i18nKey={
|
||||
'com.affine.setting.account.delete.confirm-delete-description-1'
|
||||
}
|
||||
components={{
|
||||
1: <strong />,
|
||||
}}
|
||||
values={{
|
||||
server:
|
||||
serverService.server.id !== 'affine-cloud'
|
||||
? `${serverService.server.config$.value.serverName} (${serverService.server.baseUrl})`
|
||||
: serverService.server.config$.value.serverName,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
confirmText={t['com.affine.setting.account.delete.confirm-button']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'error',
|
||||
@@ -140,7 +157,7 @@ const DeleteAccountModal = ({
|
||||
childrenContentClassName={styles.confirmContent}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="com.affine.setting.account.delete.confirm-description-2"
|
||||
i18nKey="com.affine.setting.account.delete.confirm-delete-description-2"
|
||||
components={{
|
||||
1: <strong />,
|
||||
}}
|
||||
|
||||
@@ -106,6 +106,9 @@ export const Component = () => {
|
||||
|
||||
const createSession = useCallback(
|
||||
async (options: Partial<BlockSuitePresets.AICreateSessionOptions> = {}) => {
|
||||
if (currentSession) {
|
||||
return currentSession;
|
||||
}
|
||||
const sessionId = await client.createSession({
|
||||
workspaceId,
|
||||
promptName: 'Chat With AFFiNE AI' satisfies PromptKey,
|
||||
@@ -117,7 +120,7 @@ export const Component = () => {
|
||||
setCurrentSession(session);
|
||||
return session;
|
||||
},
|
||||
[client, workspaceId]
|
||||
[client, currentSession, workspaceId]
|
||||
);
|
||||
|
||||
const togglePin = useCallback(async () => {
|
||||
@@ -153,8 +156,8 @@ export const Component = () => {
|
||||
.then(session => {
|
||||
setCurrentSession(session);
|
||||
if (chatContent) {
|
||||
chatContent.session = session;
|
||||
chatContent.reloadSession();
|
||||
chatContent.remove();
|
||||
setChatContent(null);
|
||||
}
|
||||
chatTool?.closeHistoryMenu();
|
||||
})
|
||||
@@ -204,10 +207,10 @@ export const Component = () => {
|
||||
confirmModal.closeConfirmModal,
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
content.createSession = createSession;
|
||||
|
||||
if (!chatContent) {
|
||||
// initial values that won't change
|
||||
content.createSession = createSession;
|
||||
content.independentMode = true;
|
||||
content.onboardingOffsetY = -100;
|
||||
chatContainerRef.current?.append(content);
|
||||
@@ -255,7 +258,8 @@ export const Component = () => {
|
||||
tool.onNewSession = () => {
|
||||
if (!currentSession) return;
|
||||
setCurrentSession(null);
|
||||
chatContent?.reset();
|
||||
chatContent?.remove();
|
||||
setChatContent(null);
|
||||
};
|
||||
|
||||
tool.onTogglePin = async () => {
|
||||
@@ -306,6 +310,38 @@ export const Component = () => {
|
||||
return () => sub.unsubscribe();
|
||||
}, [framework, mockStd]);
|
||||
|
||||
// restore pinned session
|
||||
useEffect(() => {
|
||||
if (!chatContent) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
client
|
||||
.getSessions(
|
||||
workspaceId,
|
||||
{},
|
||||
undefined,
|
||||
{ pinned: true, limit: 1 },
|
||||
signal
|
||||
)
|
||||
.then(sessions => {
|
||||
if (!Array.isArray(sessions)) return;
|
||||
const session = sessions[0];
|
||||
if (!session) return;
|
||||
setCurrentSession(session);
|
||||
if (chatContent) {
|
||||
chatContent.remove();
|
||||
setChatContent(null);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
// abort the request
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [chatContent, client, workspaceId]);
|
||||
|
||||
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||
if (node) {
|
||||
setIsBodyProvided(true);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const root = style({
|
||||
borderRadius: 8,
|
||||
boxShadow: cssVar('buttonShadow'),
|
||||
borderWidth: 0,
|
||||
background: cssVarV2('button/siderbarPrimary/background'),
|
||||
background: cssVarV2('button/iconButtonSolid'),
|
||||
});
|
||||
|
||||
export const withAskRoot = style([
|
||||
|
||||
@@ -25,74 +25,82 @@ export class DesktopStateSynchronizer extends Service {
|
||||
const workbench = this.workbenchService.workbench;
|
||||
const appInfo = this.electronApi.appInfo;
|
||||
|
||||
this.electronApi.events.ui.onTabAction(event => {
|
||||
if (
|
||||
event.type === 'open-in-split-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
workbench.openAll({
|
||||
at: 'beside',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'separate-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
const view = workbench.viewAt(event.payload.viewIndex);
|
||||
if (view) {
|
||||
workbench.close(view);
|
||||
this.disposables.push(
|
||||
this.electronApi.events.ui.onTabAction(event => {
|
||||
if (
|
||||
event.type === 'open-in-split-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
workbench.openAll({
|
||||
at: 'beside',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'activate-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
workbench.active(event.payload.viewIndex);
|
||||
}
|
||||
});
|
||||
|
||||
this.electronApi.events.ui.onCloseView(() => {
|
||||
(async () => {
|
||||
if (await this.electronApi.handler.ui.isActiveTab()) {
|
||||
// close current view. stop if any one is successful
|
||||
// 1. peek view
|
||||
// 2. split view
|
||||
// 3. tab
|
||||
// 4. otherwise, hide the window
|
||||
if (this.peekViewService.peekView.show$.value?.value) {
|
||||
this.peekViewService.peekView.close();
|
||||
} else if (workbench.views$.value.length > 1) {
|
||||
workbench.close(workbench.activeView$.value);
|
||||
} else {
|
||||
const tabs = await this.electronApi.handler.ui.getTabsStatus();
|
||||
if (tabs.length > 1) {
|
||||
await this.electronApi.handler.ui.closeTab();
|
||||
} else {
|
||||
await this.electronApi.handler.ui.handleHideApp();
|
||||
}
|
||||
if (
|
||||
event.type === 'separate-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
const view = workbench.viewAt(event.payload.viewIndex);
|
||||
if (view) {
|
||||
workbench.close(view);
|
||||
}
|
||||
}
|
||||
})().catch(console.error);
|
||||
});
|
||||
|
||||
this.electronApi.events.ui.onToggleRightSidebar(tabId => {
|
||||
if (tabId === appInfo?.viewId) {
|
||||
workbench.setSidebarOpen(!workbench.sidebarOpen$.value);
|
||||
}
|
||||
});
|
||||
if (
|
||||
event.type === 'activate-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
) {
|
||||
workbench.active(event.payload.viewIndex);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.electronApi.events.ui.onTabGoToRequest(opts => {
|
||||
if (opts.tabId === appInfo?.viewId) {
|
||||
this.workbenchService.workbench.open(opts.to);
|
||||
}
|
||||
});
|
||||
this.disposables.push(
|
||||
this.electronApi.events.ui.onCloseView(() => {
|
||||
(async () => {
|
||||
if (await this.electronApi.handler.ui.isActiveTab()) {
|
||||
// close current view. stop if any one is successful
|
||||
// 1. peek view
|
||||
// 2. split view
|
||||
// 3. tab
|
||||
// 4. otherwise, hide the window
|
||||
if (this.peekViewService.peekView.show$.value?.value) {
|
||||
this.peekViewService.peekView.close();
|
||||
} else if (workbench.views$.value.length > 1) {
|
||||
workbench.close(workbench.activeView$.value);
|
||||
} else {
|
||||
const tabs = await this.electronApi.handler.ui.getTabsStatus();
|
||||
if (tabs.length > 1) {
|
||||
await this.electronApi.handler.ui.closeTab();
|
||||
} else {
|
||||
await this.electronApi.handler.ui.handleHideApp();
|
||||
}
|
||||
}
|
||||
}
|
||||
})().catch(console.error);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
this.electronApi.events.ui.onToggleRightSidebar(tabId => {
|
||||
if (tabId === appInfo?.viewId) {
|
||||
workbench.setSidebarOpen(!workbench.sidebarOpen$.value);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
this.electronApi.events.ui.onTabGoToRequest(opts => {
|
||||
if (opts.tabId === appInfo?.viewId) {
|
||||
this.workbenchService.workbench.open(opts.to);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// sync workbench state with main process
|
||||
// also fill tab view meta with title & moduleName
|
||||
LiveData.computed(get => {
|
||||
const viewsSub = LiveData.computed(get => {
|
||||
return get(workbench.views$).map(view => {
|
||||
const location = get(view.location$);
|
||||
return {
|
||||
@@ -118,19 +126,21 @@ export class DesktopStateSynchronizer extends Service {
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
workbench.activeViewIndex$.subscribe(activeViewIndex => {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
const activeViewIndexSub = workbench.activeViewIndex$.subscribe(
|
||||
activeViewIndex => {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.electronApi.handler.ui
|
||||
.updateWorkbenchMeta(appInfo.viewId, {
|
||||
activeViewIndex: activeViewIndex,
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
);
|
||||
|
||||
this.electronApi.handler.ui
|
||||
.updateWorkbenchMeta(appInfo.viewId, {
|
||||
activeViewIndex: activeViewIndex,
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
workbench.basename$.subscribe(basename => {
|
||||
const basenameSub = workbench.basename$.subscribe(basename => {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
}
|
||||
@@ -141,5 +151,11 @@ export class DesktopStateSynchronizer extends Service {
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
this.disposables.push(
|
||||
() => viewsSub.unsubscribe(),
|
||||
() => activeViewIndexSub.unsubscribe(),
|
||||
() => basenameSub.unsubscribe()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4838,9 +4838,11 @@ export function useAFFiNEI18N(): {
|
||||
*/
|
||||
["com.affine.setting.account"](): string;
|
||||
/**
|
||||
* `Delete your account`
|
||||
* `Delete your account from {{server}}`
|
||||
*/
|
||||
["com.affine.setting.account.delete"](): string;
|
||||
["com.affine.setting.account.delete-from-server"](options: {
|
||||
readonly server: string;
|
||||
}): string;
|
||||
/**
|
||||
* `Once deleted, your account will no longer be accessible, and all data in your personal cloud space will be permanently deleted.`
|
||||
*/
|
||||
@@ -4857,10 +4859,6 @@ export function useAFFiNEI18N(): {
|
||||
* `Delete your account?`
|
||||
*/
|
||||
["com.affine.setting.account.delete.confirm-title"](): string;
|
||||
/**
|
||||
* `Are you sure you want to delete your account?`
|
||||
*/
|
||||
["com.affine.setting.account.delete.confirm-description-1"](): string;
|
||||
/**
|
||||
* `Please type your email to confirm`
|
||||
*/
|
||||
@@ -9291,10 +9289,18 @@ export const TypedTrans: {
|
||||
}, {
|
||||
["1"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
* `Are you sure you want to delete your account from <1>{{server}}</1>?`
|
||||
*/
|
||||
["com.affine.setting.account.delete.confirm-delete-description-1"]: ComponentType<TypedTransProps<{
|
||||
readonly server: string;
|
||||
}, {
|
||||
["1"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
* `Your account will be inaccessible, and your personal cloud space will be permanently deleted. You can remove local data by uninstalling the app or clearing your browser storage. <1>This action is irreversible.</1>`
|
||||
*/
|
||||
["com.affine.setting.account.delete.confirm-description-2"]: ComponentType<TypedTransProps<Readonly<{}>, {
|
||||
["com.affine.setting.account.delete.confirm-delete-description-2"]: ComponentType<TypedTransProps<Readonly<{}>, {
|
||||
["1"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "تعليقات",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "ستتلقى إشعارًا عبر البريد الإلكتروني عندما يعلق أعضاء آخرون في مساحة العمل على مستنداتك.",
|
||||
"com.affine.setting.account": "إعدادات الحساب",
|
||||
"com.affine.setting.account.delete": "حذف حسابك",
|
||||
"com.affine.setting.account.delete.message": "بمجرد الحذف، لن يكون حسابك متاحًا للوصول، وسيتم حذف جميع البيانات في مساحة التخزين السحابية الشخصية الخاصة بك بشكل دائم.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "لا يمكن حذف الحساب",
|
||||
"com.affine.setting.account.delete.team-warning-description": "أنت مالك لمساحة عمل الفريق. لحذف حسابك، يرجى حذف مساحة العمل أو نقل الملكية أولا.",
|
||||
"com.affine.setting.account.delete.confirm-title": "حذف حسابك؟",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "هل أنت متأكد أنك تريد حذف حسابك؟",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "لن يكون حسابك متاحًا للوصول، وسيتم حذف المساحة السحابية الشخصية الخاصة بك بشكل دائم. يمكنك إزالة البيانات المحلية عن طريق إلغاء تثبيت التطبيق أو مسح التخزين في المتصفح الخاص بك. <1>هذا الإجراء لا يمكن عكسه.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "يرجى كتابة بريدك الإلكتروني للتأكيد",
|
||||
"com.affine.setting.account.delete.confirm-button": "حذف",
|
||||
"com.affine.setting.account.delete.success-title": "تم حذف الحساب",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Kommentare",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Du wirst per E-Mail benachrichtigt, wenn andere Mitglieder des Workspace Kommentare zu deinen Seiten abgeben.",
|
||||
"com.affine.setting.account": "Kontoeinstellungen",
|
||||
"com.affine.setting.account.delete": "Dein Konto löschen",
|
||||
"com.affine.setting.account.delete.message": "Sobald gelöscht, ist dein Konto nicht mehr zugänglich, und alle Daten in deinem persönlichen Cloud-Speicher werden dauerhaft gelöscht.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Konto kann nicht gelöscht werden",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Du bist der Besitzer eines Team-Workspace. Um dein Konto zu löschen, lösche bitte zuerst den Workspace oder übertrage die Eigentümerschaft.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Dein Konto löschen?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Möchtest du dein Konto wirklich löschen?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Dein Konto wird nicht mehr zugänglich sein und dein persönlicher Cloud-Speicherplatz wird dauerhaft gelöscht. Du kannst lokale Daten entfernen, indem du die App deinstallierst oder den Speicher deines Browsers leerst. <1>Diese Aktion ist unwiderruflich.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Bitte gib zur Bestätigung deine E-Mail-Adresse ein",
|
||||
"com.affine.setting.account.delete.confirm-button": "Löschen",
|
||||
"com.affine.setting.account.delete.success-title": "Konto gelöscht",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Σχόλια",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Θα ενημερωθείτε μέσω email όταν άλλα μέλη του χώρου εργασίας σχολιάσουν τα έγγραφά σας.",
|
||||
"com.affine.setting.account": "Ρυθμίσεις λογαριασμού",
|
||||
"com.affine.setting.account.delete": "Διαγραφή του λογαριασμού σας",
|
||||
"com.affine.setting.account.delete.message": "Μόλις διαγραφεί, ο λογαριασμός σας δεν θα είναι πλέον προσβάσιμος, και όλα τα δεδομένα στο προσωπικό σας cloud θα διαγραφούν μόνιμα.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Δεν μπορεί να διαγραφεί λογαριασμός",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Είστε ο ιδιοκτήτης ενός χώρου εργασίας ομάδας. Για να διαγράψετε το λογαριασμό σας, παρακαλώ διαγράψτε το χώρο εργασίας ή μεταφέρετε την ιδιοκτησία πρώτα.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Διαγραφή του λογαριασμού σας;",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το λογαριασμό σας;",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Ο λογαριασμός σας δεν θα είναι προσβάσιμος, και ο προσωπικός σας cloud χώρος θα διαγραφεί μόνιμα. Μπορείτε να αφαιρέσετε τα τοπικά δεδομένα απεγκαθιστώντας την εφαρμογή ή καθαρίζοντας τον αποθηκευτικό χώρο του προγράμματος περιήγησης. <1>Αυτή η ενέργεια είναι μη αναστρέψιμη.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Παρακαλώ πληκτρολογήστε το email σας για επιβεβαίωση",
|
||||
"com.affine.setting.account.delete.confirm-button": "Διαγραφή",
|
||||
"com.affine.setting.account.delete.success-title": "Ο λογαριασμός διαγράφτηκε",
|
||||
|
||||
@@ -1202,13 +1202,13 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Comments",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "You will be notified through email when other members of the workspace comment on your docs.",
|
||||
"com.affine.setting.account": "Account settings",
|
||||
"com.affine.setting.account.delete": "Delete your account",
|
||||
"com.affine.setting.account.delete-from-server": "Delete your account from {{server}}",
|
||||
"com.affine.setting.account.delete.message": "Once deleted, your account will no longer be accessible, and all data in your personal cloud space will be permanently deleted.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Cannot delete account",
|
||||
"com.affine.setting.account.delete.team-warning-description": "You’re the owner of a team workspace. To delete your account, please delete the workspace or transfer ownership first.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Delete your account?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Are you sure you want to delete your account?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Your account will be inaccessible, and your personal cloud space will be permanently deleted. You can remove local data by uninstalling the app or clearing your browser storage. <1>This action is irreversible.</1>",
|
||||
"com.affine.setting.account.delete.confirm-delete-description-1": "Are you sure you want to delete your account from <1>{{server}}</1>?",
|
||||
"com.affine.setting.account.delete.confirm-delete-description-2": "Your account will be inaccessible, and your personal cloud space will be permanently deleted. You can remove local data by uninstalling the app or clearing your browser storage. <1>This action is irreversible.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Please type your email to confirm",
|
||||
"com.affine.setting.account.delete.confirm-button": "Delete",
|
||||
"com.affine.setting.account.delete.success-title": "Account deleted",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Comentarios",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Se te notificará por correo electrónico cuando otros miembros del espacio de trabajo comenten tus documentos.",
|
||||
"com.affine.setting.account": "Configuración de la cuenta",
|
||||
"com.affine.setting.account.delete": "Eliminar su cuenta",
|
||||
"com.affine.setting.account.delete.message": "Una vez eliminado, su cuenta ya no será accesible y todos los datos en su espacio personal en la nube serán eliminados permanentemente.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "No se puede eliminar la cuenta",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Usted es el propietario de un espacio de trabajo en equipo. Para eliminar su cuenta, elimine el espacio de trabajo o transfiera la propiedad primero.",
|
||||
"com.affine.setting.account.delete.confirm-title": "¿Eliminar su cuenta?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "¿Está seguro de que desea eliminar su cuenta?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Su cuenta será inaccesible y su espacio personal en la nube será eliminado permanentemente. Puede eliminar datos locales desinstalando la aplicación o borrando el almacenamiento de su navegador. <1>Esta acción es irreversible.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Por favor, ingrese su correo electrónico para confirmar",
|
||||
"com.affine.setting.account.delete.confirm-button": "Borrar",
|
||||
"com.affine.setting.account.delete.success-title": "Cuenta eliminada",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "نظرات",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "هنگامی که اعضای دیگر فضای کار بر روی اسناد شما نظر میدهند، از طریق ایمیل مطلع خواهید شد.",
|
||||
"com.affine.setting.account": "تنظیمات حساب",
|
||||
"com.affine.setting.account.delete": "حذف حساب خود",
|
||||
"com.affine.setting.account.delete.message": "پس از حذف، حساب شما دیگر قابل دسترسی نیست و همه دادهها در فضای ابری شخصی شما بهطور دائم حذف خواهند شد.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "نمیتوان حساب را حذف کرد",
|
||||
"com.affine.setting.account.delete.team-warning-description": "شما مالک فضای کار تیمی هستید. برای حذف حساب خود، لطفاً ابتدا فضای کار را حذف کنید یا مالکیت را انتقال دهید.",
|
||||
"com.affine.setting.account.delete.confirm-title": "آیا حساب خود را حذف کنید؟",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "آیا از حذف حساب خود اطمینان دارید؟",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "حساب شما غیرقابل دسترسی خواهد شد و فضای ابری شخصی شما بهطور دائم حذف خواهد شد. شما میتوانید دادههای محلی را با حذف نصب برنامه یا پاک کردن حافظه مرورگر خود حذف کنید.<1>این عمل غیرقابل برگشت است.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "لطفاً ایمیل خود را برای تأیید وارد کنید",
|
||||
"com.affine.setting.account.delete.confirm-button": "حذف",
|
||||
"com.affine.setting.account.delete.success-title": "حساب کاربری حذف شد",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Commentaires",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Vous serez averti par email lorsque d'autres membres de l'espace de travail commenteront vos documents.",
|
||||
"com.affine.setting.account": "Paramètres du compte",
|
||||
"com.affine.setting.account.delete": "Supprimer votre compte",
|
||||
"com.affine.setting.account.delete.message": "Une fois supprimé, votre compte ne sera plus accessible, et toutes les données de votre espace cloud personnel seront supprimées définitivement.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Impossible de supprimer le compte",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Vous êtes le propriétaire d'un espace de travail d'équipe. Pour supprimer votre compte, veuillez d'abord supprimer l'espace de travail ou transférer la propriété.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Supprimer votre compte ?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Êtes-vous sûr(e) de vouloir supprimer votre compte ?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Votre compte sera inaccessible, et votre espace cloud personnel sera supprimé définitivement. Vous pouvez supprimer les données locales en désinstallant l'application ou en vidant le stockage de votre navigateur. <1>Cette action est irréversible.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Veuillez entrer votre email pour confirmer",
|
||||
"com.affine.setting.account.delete.confirm-button": "Supprimer",
|
||||
"com.affine.setting.account.delete.success-title": "Compte supprimé",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Commenti",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Riceverai una notifica tramite e-mail quando altri membri dello spazio di lavoro commentano i tuoi documenti.",
|
||||
"com.affine.setting.account": "Impostazioni account",
|
||||
"com.affine.setting.account.delete": "Elimina il tuo account",
|
||||
"com.affine.setting.account.delete.message": "Una volta eliminato, il tuo account non sarà più accessibile e tutti i dati nel tuo spazio cloud personale verranno eliminati permanentemente.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Impossibile eliminare account",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Sei il proprietario di uno spazio di lavoro del team. Per eliminare il tuo account, elimina prima lo spazio di lavoro o trasferisci la proprietà.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Eliminare il tuo account?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Sei sicuro di voler eliminare il tuo account?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Il tuo account diventerà inaccessibile e il tuo spazio cloud personale verrà eliminato permanentemente. Puoi rimuovere i dati locali disinstallando l'app o cancellando la memorizzazione nel browser. <1>Questa azione è irreversibile.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Per favore digita la tua email per confermare",
|
||||
"com.affine.setting.account.delete.confirm-button": "Elimina",
|
||||
"com.affine.setting.account.delete.success-title": "Account eliminato",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "コメント",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "ワークスペースの他のメンバーがあなたのドキュメントにコメントを残したときに、メールで通知されます。",
|
||||
"com.affine.setting.account": "アカウント設定",
|
||||
"com.affine.setting.account.delete": "アカウントを削除",
|
||||
"com.affine.setting.account.delete.message": "削除後は、アカウントにアクセスできなくなり、個人のクラウドスペース内のすべてのデータが永久に削除されます。",
|
||||
"com.affine.setting.account.delete.team-warning-title": "アカウントを削除できません",
|
||||
"com.affine.setting.account.delete.team-warning-description": "あなたはチームワークスペースの所有者です。アカウントを削除するには、まずワークスペースを削除または所有権を譲渡してください。",
|
||||
"com.affine.setting.account.delete.confirm-title": "アカウントを削除しますか?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "本当にアカウントを削除しますか?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "アカウントにアクセスできなくなり、個人のクラウドスペースが永久に削除されます。ローカルデータはアプリをアンインストールするか、ブラウザストレージをクリアすることで削除できます。<1>このアクションは元に戻せません。</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "確認するにはメールアドレスを入力してください",
|
||||
"com.affine.setting.account.delete.confirm-button": "削除",
|
||||
"com.affine.setting.account.delete.success-title": "アカウントが削除されました",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Komentarze",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Zostaniesz powiadomiony e-mailem, gdy inni członkowie przestrzeni roboczej skomentują twoje dokumenty.",
|
||||
"com.affine.setting.account": "Ustawienia konta",
|
||||
"com.affine.setting.account.delete": "Usuń swoje konto",
|
||||
"com.affine.setting.account.delete.message": "Po usunięciu Twoje konto nie będzie dostępne, a wszystkie dane w Twojej osobistej przestrzeni chmury zostaną trwale usunięte.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Nie można usunąć konta",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Jesteś właścicielem przestrzeni roboczej zespołu. Aby usunąć swoje konto, usuń najpierw przestrzeń roboczą lub przenieś własność.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Usunąć swoje konto?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Czy na pewno chcesz usunąć swoje konto?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Twoje konto będzie niedostępne, a Twoja osobista przestrzeń chmury zostanie trwale usunięta. Możesz usunąć dane lokalne poprzez odinstalowanie aplikacji lub usunięcie danych przeglądarki. <1>Tej akcji nie można cofnąć.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Wpisz swój e-mail, aby potwierdzić",
|
||||
"com.affine.setting.account.delete.confirm-button": "Usuń",
|
||||
"com.affine.setting.account.delete.success-title": "Konto usunięte",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Comentários",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Você será notificado por email quando outros membros do espaço de trabalho comentarem em seus documentos.",
|
||||
"com.affine.setting.account": "Configurações da conta",
|
||||
"com.affine.setting.account.delete": "Excluir sua conta",
|
||||
"com.affine.setting.account.delete.message": "Após ser excluída, sua conta não será mais acessível, e todos os dados em seu espaço na nuvem pessoal serão excluídos permanentemente.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Não é possível excluir conta",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Você é o proprietário de um ambiente de trabalho em equipe. Para excluir sua conta, por favor, exclua o ambiente de trabalho ou transfira a propriedade primeiro.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Excluir sua conta?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Tem certeza de que deseja excluir sua conta?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Sua conta ficará inacessível, e seu espaço na nuvem pessoal será excluído permanentemente. Você pode remover dados locais desinstalando o aplicativo ou limpando o armazenamento do seu navegador. <1>Esta ação é irreversível.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Por favor, digite seu email para confirmar",
|
||||
"com.affine.setting.account.delete.confirm-button": "Excluir",
|
||||
"com.affine.setting.account.delete.success-title": "Conta excluída",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Комментарии",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Вы будете получать уведомления по электронной почте, когда другие члены рабочего пространства комментируют ваши документы.",
|
||||
"com.affine.setting.account": "Настройки учётной записи",
|
||||
"com.affine.setting.account.delete": "Удалить ваш аккаунт",
|
||||
"com.affine.setting.account.delete.message": "После удаления ваш аккаунт станет недоступным, и все данные в вашем личном облачном пространстве будут удалены навсегда.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Невозможно удалить аккаунт",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Вы являетесь владельцем рабочего пространства команды. Чтобы удалить ваш аккаунт, сначала удалите рабочее пространство или передайте права владения.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Удалить ваш аккаунт?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Вы уверены, что хотите удалить ваш аккаунт?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Ваш аккаунт станет недоступным, а ваше личное облачное пространство будет удалено навсегда. Вы можете удалить локальные данные, удалив приложение или очистив хранилище браузера. <1>Это действие не может быть отменено.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Пожалуйста, введите вашу электронную почту для подтверждения",
|
||||
"com.affine.setting.account.delete.confirm-button": "Удалить",
|
||||
"com.affine.setting.account.delete.success-title": "Аккаунт удален",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Kommentarer",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Du kommer att bli meddelad via e-post när andra medlemmar i arbetsytan kommenterar dina dokument.",
|
||||
"com.affine.setting.account": "Kontoinställningar",
|
||||
"com.affine.setting.account.delete": "Radera ditt konto",
|
||||
"com.affine.setting.account.delete.message": "När raderad, kommer ditt konto inte längre vara åtkomligt, och alla data i ditt personliga molnlagringsutrymme kommer permanent raderas.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Kan inte radera konto",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Du är ägare till en teamarbetsyta. För att radera ditt konto, vänligen ta bort arbetsytan eller överför ägarskapet först.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Radera ditt konto?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Är du säker på att du vill radera ditt konto?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Ditt konto kommer vara oåtkomligt, och ditt personliga molnutrymme kommer permanent raderas. Du kan ta bort lokala data genom att avinstallera appen eller tömma din webbläsares lagring. <1>Denna åtgärd är oåterkallelig.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Vänligen skriv din e-post för att bekräfta",
|
||||
"com.affine.setting.account.delete.confirm-button": "Radera",
|
||||
"com.affine.setting.account.delete.success-title": "Konto raderat",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "Коментарі",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "Ви будете повідомлені електронною поштою, коли інші учасники робочого простору коментуватимуть ваші документи.",
|
||||
"com.affine.setting.account": "Налаштування облікового запису",
|
||||
"com.affine.setting.account.delete": "Видалити ваш аккаунт",
|
||||
"com.affine.setting.account.delete.message": "Після видалення ваш обліковий запис буде недоступним, а всі дані в вашому особистому хмарному просторі будуть остаточно видалені.",
|
||||
"com.affine.setting.account.delete.team-warning-title": "Не можна видалити аккаунт",
|
||||
"com.affine.setting.account.delete.team-warning-description": "Ви є власником робочого простору команди. Щоб видалити свій аккаунт, спочатку видаліть робочий простір або передайте власність.",
|
||||
"com.affine.setting.account.delete.confirm-title": "Видалити ваш аккаунт?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "Ви впевнені, що хочете видалити свій аккаунт?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "Ваш обліковий запис стане недоступним, а ваш особистий хмарний простір буде остаточно видалено. Ви можете видалити локальні дані, видаливши програму або очистивши пам'ять браузера. <1>Ця дія незворотна.</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "Будь ласка, введіть свій електронний лист для підтвердження",
|
||||
"com.affine.setting.account.delete.confirm-button": "Видалити",
|
||||
"com.affine.setting.account.delete.success-title": "Аккаунт видалено",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "评论",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "当工作区的其他成员评论你的文档时,你将通过电子邮件收到通知。",
|
||||
"com.affine.setting.account": "账号设置",
|
||||
"com.affine.setting.account.delete": "删除您的账号",
|
||||
"com.affine.setting.account.delete.message": "一旦删除,您的账户将无法访问,您个人云空间中的所有数据将被永久删除。",
|
||||
"com.affine.setting.account.delete.team-warning-title": "无法删除帐户",
|
||||
"com.affine.setting.account.delete.team-warning-description": "您是团队工作区的拥有者。要删除您的帐户,请先删除工作区或转移所有权。",
|
||||
"com.affine.setting.account.delete.confirm-title": "删除您的账户?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "您确定要删除您的账户吗?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "您的账号将无法访问,您的个人云空间将被永久删除。您可以通过卸载应用程序或清除浏览器存储来移除本地数据。<1>此操作不可逆。</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "请输入您的电子邮件以确认",
|
||||
"com.affine.setting.account.delete.confirm-button": "删除",
|
||||
"com.affine.setting.account.delete.success-title": "账户已删除",
|
||||
|
||||
@@ -1202,13 +1202,10 @@
|
||||
"com.affine.setting.notifications.email.comments.title": "評論",
|
||||
"com.affine.setting.notifications.email.comments.subtitle": "當工作區的其他成員評論你的文檔時,你將通過電子郵件收到通知。",
|
||||
"com.affine.setting.account": "帳號設定",
|
||||
"com.affine.setting.account.delete": "刪除您的賬號",
|
||||
"com.affine.setting.account.delete.message": "一旦刪除,您的賬戶將無法訪問,您個人雲空間中的所有數據將被永久刪除。",
|
||||
"com.affine.setting.account.delete.team-warning-title": "無法刪除帳戶",
|
||||
"com.affine.setting.account.delete.team-warning-description": "您是團隊工作區的擁有者。要刪除您的帳戶,請先刪除工作區或轉移所有權。",
|
||||
"com.affine.setting.account.delete.confirm-title": "刪除您的賬戶?",
|
||||
"com.affine.setting.account.delete.confirm-description-1": "您確定要刪除您的賬戶嗎?",
|
||||
"com.affine.setting.account.delete.confirm-description-2": "您的賬號將無法訪問,您的個人雲空間將被永久刪除。您可以通過卸載應用程式或清除瀏覽器存儲來移除本地數據。<1>此操作不可逆。</1>",
|
||||
"com.affine.setting.account.delete.input-placeholder": "請輸入您的電子郵件以確認",
|
||||
"com.affine.setting.account.delete.confirm-button": "刪除",
|
||||
"com.affine.setting.account.delete.success-title": "賬戶已刪除",
|
||||
|
||||
@@ -442,6 +442,9 @@ interface PageEvents extends PageDivision {
|
||||
chatPanel: {
|
||||
chatPanelInput: ['addEmbeddingDoc'];
|
||||
};
|
||||
intelligence: {
|
||||
chatPanelInput: ['addEmbeddingDoc'];
|
||||
};
|
||||
commentPanel: {
|
||||
$: ['createComment', 'editComment', 'deleteComment', 'resolveComment'];
|
||||
};
|
||||
@@ -720,7 +723,7 @@ export type EventArgs = {
|
||||
addEmbeddingDoc: {
|
||||
type?: 'page' | 'edgeless';
|
||||
control: 'addButton' | 'atMenu';
|
||||
method: 'doc' | 'file' | 'tags' | 'collections' | 'suggestion';
|
||||
method: 'doc' | 'cur-doc' | 'file' | 'tags' | 'collections' | 'suggestion';
|
||||
};
|
||||
openAttachmentInFullscreen: AttachmentEventArgs;
|
||||
openAttachmentInNewTab: AttachmentEventArgs;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { enableAutoTrack, makeTracker } from './auto';
|
||||
import { type Events } from './events';
|
||||
import { type EventArgs, type Events } from './events';
|
||||
import { mixpanel } from './mixpanel';
|
||||
import { sentry } from './sentry';
|
||||
export const track = makeTracker((event, props) => {
|
||||
mixpanel.track(event, props);
|
||||
});
|
||||
|
||||
export { enableAutoTrack, type Events, mixpanel, sentry };
|
||||
export { enableAutoTrack, type EventArgs, type Events, mixpanel, sentry };
|
||||
export default track;
|
||||
|
||||
@@ -59,7 +59,7 @@ test.describe('AIAction/ChangeTone', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ test.describe('AIAction/CheckCodeError', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ test.describe('AIAction/ContinueWriting', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('AIAction/ExplainCode', () => {
|
||||
await expect(answer).toHaveText(/console.log/);
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('AIAction/ExplainImage', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('AIAction/ExplainSelection', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -102,7 +102,7 @@ Compare and Select Flights`
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('AIAction/FixGrammar', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -50,7 +50,7 @@ test.describe('AIAction/FixSpelling', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('AIAction/GenerateAnImageWithImage', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -63,7 +63,7 @@ test.describe('AIAction/GenerateAnImageWithText', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('AIAction/GenerateHeadings', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ test.describe('AIAction/GenerateImageCaption', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('AIAction/GenerateOutline', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('AIAction/ImageProcessing', () => {
|
||||
await expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -51,7 +51,7 @@ test.describe('AIAction/ImproveWriting', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('AIAction/MakeItLonger', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -64,7 +64,7 @@ test.describe('AIAction/MakeItReal', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('AIAction/MakeItShorter', () => {
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({
|
||||
test.skip('should show chat history in chat panel', async ({
|
||||
loggedInPage: page,
|
||||
utils,
|
||||
}) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user