Skip to content

Commit

Permalink
feat: add reasoning (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
xieziyu authored May 6, 2023
1 parent 5f5b1dc commit dcaf021
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 43 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ With the ChatGPT SVG Creator extension, you can design and modify SVGs using nat

## Demo

#### Create a new SVG and modify it
![demo1](./docs/svg-creator-demo-1.gif)

#### Import an existing SVG and modify it
![demo2](./docs/svg-creator-demo-2.gif)
![demo1](./docs/demo-1.png)

## Manual Installation

Expand Down
6 changes: 1 addition & 5 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@

## 演示

#### 创建一个新的 SVG 并修改
![demo1](./docs/svg-creator-demo-1.gif)

#### 导入已有的 SVG 并修改
![demo2](./docs/svg-creator-demo-2.gif)
![demo1](./docs/demo-1.png)

## 手动安装

Expand Down
3 changes: 3 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-markup.min.js",
"node_modules/prismjs/components/prism-markdown.min.js",
"node_modules/prismjs/components/prism-json.min.js",
"node_modules/prismjs/components/prism-javascript.min.js",
"node_modules/clipboard/dist/clipboard.min.js"
]
},
Expand Down
Binary file added docs/demo-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/svg-creator-demo-1.gif
Binary file not shown.
Binary file removed docs/svg-creator-demo-2.gif
Binary file not shown.
13 changes: 11 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';

import { MarkdownModule } from 'ngx-markdown';
import { MarkdownModule, MarkedOptions } from 'ngx-markdown';
import { CardModule } from 'primeng/card';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { PasswordModule } from 'primeng/password';
Expand All @@ -19,6 +19,7 @@ import { FileUploadModule } from 'primeng/fileupload';
import { SidebarModule } from 'primeng/sidebar';
import { ToastModule } from 'primeng/toast';
import { MessageService } from 'primeng/api';
import { TagModule } from 'primeng/tag';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
Expand All @@ -40,7 +41,14 @@ import { SettingsComponent } from './components/settings/settings.component';
imports: [
FormsModule,
BrowserModule,
MarkdownModule.forRoot(),
MarkdownModule.forRoot({
markedOptions: {
provide: MarkedOptions,
useValue: {
smartLists: true,
},
},
}),
BrowserAnimationsModule,
HttpClientModule,
AppRoutingModule,
Expand All @@ -57,6 +65,7 @@ import { SettingsComponent } from './components/settings/settings.component';
FileUploadModule,
SidebarModule,
ToastModule,
TagModule,
],
providers: [MessageService],
bootstrap: [AppComponent],
Expand Down
17 changes: 16 additions & 1 deletion src/app/components/svg-creator/svg-creator.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,26 @@ <h5>Input</h5>
(click)="stop()"></button>
</div>
</div>
<div class="card" *ngIf="reasoning && !rawResp">
<h5>Reasoning</h5>
<div class="flex gap-2" *ngIf="keywords.length > 0">
<p-tag *ngFor="let keyword of keywords" [value]="keyword"></p-tag>
</div>
<p-divider></p-divider>
<markdown ngPreserveWhitespaces [data]="reasoning"></markdown>
</div>
<div class="card" *ngIf="rawResp">
<h5>Thinking</h5>
<markdown
ngPreserveWhitespaces
clipboard
[data]="rawResp | language : 'javascript'"></markdown>
</div>
</div>

<div class="col-12 md:col-6">
<div class="card">
<h5>SVG {{ previewTitle }}</h5>
<h5>SVG Preview</h5>
<markdown ngPreserveWhitespaces clipboard [data]="svgCode | language : 'svg'"></markdown>
<p-divider></p-divider>
<div class="flex justify-content-center flex-wrap gap-2">
Expand Down
38 changes: 26 additions & 12 deletions src/app/components/svg-creator/svg-creator.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { CreatorService } from '../../services/creator.service';

Expand All @@ -12,7 +12,10 @@ export class SvgCreatorComponent implements OnInit, OnDestroy {
sanitizedSvg: SafeHtml = '';

submitting = false;
previewTitle = 'Preview';

keywords: string[] = [];
reasoning = '';
rawResp = '';

@ViewChild('svgPreview', { static: true })
svgPreview?: ElementRef<HTMLElement>;
Expand All @@ -36,19 +39,20 @@ export class SvgCreatorComponent implements OnInit, OnDestroy {
}

ngOnInit() {
this.previewTitle = 'Preview';
this.keywords = [];
this.reasoning = '';
this.creator.svgCode$.subscribe(rs => {
if (rs.done) {
this.rawResp = '';
// SSE 结束后统一解析
const svgCodes = this.creator.extractSVGCode(rs.content);
if (svgCodes.length) {
this.svgCode = svgCodes.join('\n');
}
const rsp = this.creator.extractResult(rs.content);
this.svgCode = rsp.SVG;
this.keywords = rsp.Keywords;
this.reasoning = rsp.Reasoning;
console.log(rsp);
} else {
// 临时显示输出
this._svgCode = rs.content
.replace(/[\s\S]*(?=(<svg))/gi, '')
.replace(/(?<=(\/svg>))[\s\S]*/g, '');
this.rawResp = rs.content;
}
});
}
Expand All @@ -57,9 +61,18 @@ export class SvgCreatorComponent implements OnInit, OnDestroy {
this.creator.svgCode$.unsubscribe();
}

@HostListener('document:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
this.submit();
}
}

async submit() {
if (this.submitting) {
return;
}
this.submitting = true;
this.previewTitle = 'Generating...';
let svgCodes = this.creator.extractSVGCode(this.userInput);
if (svgCodes.length) {
this.svgCode = svgCodes.join('\n');
Expand All @@ -70,7 +83,6 @@ export class SvgCreatorComponent implements OnInit, OnDestroy {
await this.creator.analyzeInputStreaming(this.userInput, this.svgCode);
}
this.submitting = false;
this.previewTitle = 'Preview';
}

stop() {
Expand All @@ -81,6 +93,8 @@ export class SvgCreatorComponent implements OnInit, OnDestroy {
this.userInput = '';
this.svgCode = '';
this.sanitizedSvg = '';
this.keywords = [];
this.reasoning = '';
}

exportSVG() {
Expand Down
41 changes: 24 additions & 17 deletions src/app/services/creator.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { Message, StreamingResult } from '../types';
import { StreamingResult, PromptResponse } from '../types';
import { ChatGPTAPIService } from './chatgpt-api.service';
import { DefaultPrompt, GivenSVGPrompt } from './svg-prompt';

const SVG_REG_EXP: RegExp = /<svg[\s\S]*?<\/svg>/gi;

Expand All @@ -10,34 +11,24 @@ const SVG_REG_EXP: RegExp = /<svg[\s\S]*?<\/svg>/gi;
})
export class CreatorService {
public svgCode$ = new Subject<StreamingResult>();

private readonly systemPrompts: Message[] = [
{
role: 'user',
content:
'Please summarize and generate SVG code based on the description, and only return the SVG code itself. Reply "ok" to confirm.',
},
{
role: 'assistant',
content: 'ok',
},
];
private abortCtl?: AbortController;

constructor(private api: ChatGPTAPIService) {}

async analyzeInputStreaming(msg: string, originalSVGCode: string) {
let headMsg = '';
let finalMsg = '';
if (originalSVGCode) {
headMsg += `Given the original SVG: ${originalSVGCode}\n`;
finalMsg = GivenSVGPrompt.replace(/\{svg\}/g, originalSVGCode);
} else {
finalMsg = DefaultPrompt;
}
finalMsg = finalMsg.replace(/\{input\}/g, msg);
this.abortCtl = new AbortController();
const rsp = await this.api.doChatStream(
[
...this.systemPrompts,
{
role: 'user',
content: headMsg + msg,
content: finalMsg,
},
],
this.svgCode$,
Expand All @@ -62,4 +53,20 @@ export class CreatorService {
extractNonSVGCode(str: string) {
return str.replace(SVG_REG_EXP, '').trim();
}

// 解析结果
extractResult(str: string): PromptResponse {
try {
const res: PromptResponse = JSON.parse(str);
return res;
} catch (err) {
// 非标准 JSON 格式, fallback:
const svg = this.extractSVGCode(str);
return {
Keywords: [],
Reasoning: 'unknown',
SVG: svg.length ? svg.join('\n') : '',
};
}
}
}
28 changes: 28 additions & 0 deletions src/app/services/svg-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const DefaultPrompt = `Follow the steps below to generate SVG code to meet my requirement which is delimited with triple backticks.
- Requirement: \`\`\`{input}\`\`\`
- Keywords: extract the keywords in the requirement. These keywords should be very helpful for generating the final SVG image.
- Reasoning: explain steps to use svg to meet these keywords. Format your reasoning as the unordered list. Make your reasoning using the same language as the requirement.
- SVG: generate the SVG code to meet the requirement refer to your reasoning.
Format your response as a JSON object with "Keywords", "Reasoning", and "SVG" as the keys.
Format "Keywords" value as string arrays.
Format "Reasoning" value as a string.
Ensure the response contains only the JSON object.
`;

export const GivenSVGPrompt = `Follow the steps below to update original SVG code to meet my requirement which is delimited with triple backticks.
- Original SVG: {svg}
- Requirement: \`\`\`{input}\`\`\`
- Keywords: extract the keywords in the requirement. These keywords should be very helpful for generating the final SVG image.
- Reasoning: explain steps to use svg to meet these keywords. Format your reasoning as the unordered list. Make your reasoning using the same language as the requirement.
- SVG: generate the SVG code to meet the requirement refer to your reasoning.
Format your response as a JSON object with "Keywords", "Reasoning", and "SVG" as the keys.
Format "Keywords" value as string arrays.
Format "Reasoning" value as a string.
Ensure the response contains only the JSON object.
`;
6 changes: 6 additions & 0 deletions src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ export const enum GPTModels {
GPT_4 = 'gpt-4',
GPT_4_32K = 'gpt-4-32k',
}

export interface PromptResponse {
Keywords: string[];
Reasoning: string;
SVG: string;
}
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ChatGPT SVG Creator",
"version": "1.1.0",
"version": "1.2.0",
"description": "An extension uses ChatGPT to create and preview SVG",
"manifest_version": 3,
"action": {},
Expand Down

0 comments on commit dcaf021

Please sign in to comment.