Explorar el Código

fix: story-generator add submit

祝雨婧 hace 3 meses
padre
commit
2e82aa3450
Se han modificado 27 ficheros con 889 adiciones y 93 borrados
  1. 0 0
      chapter-generator.page.html
  2. 38 0
      novel-app/src/app/ai-continue-modal/ai-continue-modal.component.html
  3. 59 0
      novel-app/src/app/ai-continue-modal/ai-continue-modal.component.scss
  4. 24 0
      novel-app/src/app/ai-continue-modal/ai-continue-modal.component.spec.ts
  5. 89 0
      novel-app/src/app/ai-continue-modal/ai-continue-modal.component.ts
  6. 38 0
      novel-app/src/app/ai-expand-modal/ai-expand-modal.component.html
  7. 59 0
      novel-app/src/app/ai-expand-modal/ai-expand-modal.component.scss
  8. 24 0
      novel-app/src/app/ai-expand-modal/ai-expand-modal.component.spec.ts
  9. 93 0
      novel-app/src/app/ai-expand-modal/ai-expand-modal.component.ts
  10. 38 0
      novel-app/src/app/ai-polish-modal/ai-polish-modal.component.html
  11. 59 0
      novel-app/src/app/ai-polish-modal/ai-polish-modal.component.scss
  12. 24 0
      novel-app/src/app/ai-polish-modal/ai-polish-modal.component.spec.ts
  13. 89 0
      novel-app/src/app/ai-polish-modal/ai-polish-modal.component.ts
  14. 1 1
      novel-app/src/app/app.component.spec.ts
  15. 2 2
      novel-app/src/app/app.routes.ts
  16. 1 0
      novel-app/src/app/chapter-generator/README.md
  17. 11 3
      novel-app/src/app/chapter-generator/chapter-generator.page.html
  18. 83 13
      novel-app/src/app/chapter-generator/chapter-generator.page.ts
  19. 1 1
      novel-app/src/app/character/character.page.ts
  20. 22 13
      novel-app/src/app/home/home.page.html
  21. 8 13
      novel-app/src/app/home/home.page.scss
  22. 54 5
      novel-app/src/app/home/home.page.ts
  23. 36 0
      novel-app/src/app/services/story.service.ts
  24. 4 8
      novel-app/src/app/short-generator/short-generator.page.html
  25. 8 12
      novel-app/src/app/short-generator/short-generator.page.ts
  26. 14 14
      novel-app/src/app/story-generator/story-generator.page.html
  27. 10 8
      novel-app/src/app/story-generator/story-generator.page.ts

+ 0 - 0
chapter-generator.page.html


+ 38 - 0
novel-app/src/app/ai-continue-modal/ai-continue-modal.component.html

@@ -0,0 +1,38 @@
+<!-- ai-continue-modal.component.html -->
+<ion-header>
+  <ion-toolbar>
+    <ion-title>AI 续写</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="dismiss()">关闭</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <!-- 文本域:生成提示词 -->
+  <ion-textarea [value]="userPrompt" (ionInput)="promptInput($event)" placeholder="请输入想要续写的内容~"
+    autoGrow="true"></ion-textarea>
+
+  <!-- 按钮:执行消息生成函数 -->
+  <ion-button (click)="sendMessage()" expand="block" class="primary">开始续写</ion-button>
+
+  <!-- 展示:返回消息内容 -->
+  <!-- 消息传输过程中,实时预览 -->
+  @if(!isComplete){
+  <div>{{responseMsg}}</div>
+  }
+  <!-- 消息传输完成后,实时预览Markdown格式 -->
+  @if(isComplete){
+  <fm-markdown-preview class="content-style" [content]="responseMsg"></fm-markdown-preview>
+  <ion-textarea [(ngModel)]="continuedText" rows="10" placeholder="续写后的内容" autoGrow="true"></ion-textarea>
+
+  <!-- 添加复制按钮 -->
+  <ion-button (click)="copyContinuedText()" class="secondary">复制</ion-button>
+
+  <!-- 添加填入文末按钮 -->
+  <ion-button (click)="insertIntoChapterEnd()" class="secondary">填入文末</ion-button>
+
+  <!-- 添加重新生成按钮 -->
+  <ion-button (click)="regenerate()" class="success">重新生成</ion-button>
+  }
+</ion-content>

+ 59 - 0
novel-app/src/app/ai-continue-modal/ai-continue-modal.component.scss

@@ -0,0 +1,59 @@
+/* ai-expand-modal.component.scss */
+
+ion-header {
+    ion-toolbar {
+        background-color: #f8f9fa;
+        color: black;
+
+        ion-title {
+            font-size: 1.5rem;
+        }
+
+        ion-buttons ion-button {
+            --color: red;
+        }
+    }
+}
+
+ion-content {
+    padding: 20px;
+
+    ion-textarea {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+
+    ion-button {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+
+        &.primary {
+            background-color: #007bff;
+            --color: white;
+        }
+
+        &.secondary {
+            background-color: #6c757d;
+            --color: white;
+        }
+
+        &.success {
+            background-color: #28a745;
+            --color: white;
+        }
+    }
+
+    .content-style {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+}

+ 24 - 0
novel-app/src/app/ai-continue-modal/ai-continue-modal.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { AiContinueModalComponent } from './ai-continue-modal.component';
+
+describe('AiContinueModalComponent', () => {
+  let component: AiContinueModalComponent;
+  let fixture: ComponentFixture<AiContinueModalComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [ AiContinueModalComponent ],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AiContinueModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 89 - 0
novel-app/src/app/ai-continue-modal/ai-continue-modal.component.ts

@@ -0,0 +1,89 @@
+// ai-continue-modal.component.ts
+import { Component, Input } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+import { IonButton, IonIcon, IonInput, IonItem, IonLabel, IonToolbar, IonHeader, IonContent, IonTitle, IonButtons, IonTextarea } from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms';
+import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-ai-continue-modal',
+  templateUrl: './ai-continue-modal.component.html',
+  styleUrls: ['./ai-continue-modal.component.scss'],
+  standalone: true,
+  imports: [
+    IonButton, IonIcon, IonInput, IonItem, IonTitle, FormsModule,
+    IonLabel, IonToolbar, IonHeader, IonContent, IonButtons, MarkdownPreviewModule, IonTextarea
+  ]
+})
+export class AiContinueModalComponent {
+  inputText: string = '';
+  continuedText: string = '';
+  userPrompt: string = "";
+  responseMsg: any = "";
+  isComplete: boolean = false;
+
+  @Input() onInsertText?: (text: string) => void;
+
+  constructor(private modalCtrl: ModalController) { }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+
+  promptInput(ev: any) {
+    this.userPrompt = ev.detail.value;
+  }
+
+  sendMessage() {
+    console.log("create");
+
+    let PromptTemplate = `
+    您是一名专业的文字作者,请您根据用户提供的文本继续创作。
+    以下是用户的原文:${this.userPrompt}
+    `;
+
+    let completion = new FmodeChatCompletion([
+      { role: "system", content: "" },
+      { role: "user", content: PromptTemplate }
+    ]);
+
+    completion.sendCompletion().subscribe((message: any) => {
+      // 打印消息体
+      console.log(message.content);
+      // 赋值消息内容给组件内属性
+      this.responseMsg = message.content;
+      this.continuedText = message.content; // 将生成的内容赋值给 continuedText
+      if (message?.complete) { // 判断message为完成状态,则设置isComplete为完成
+        this.isComplete = true;
+      }
+    });
+  }
+
+  copyContinuedText() {
+    navigator.clipboard.writeText(this.continuedText).then(() => {
+      alert('文本已复制到剪贴板');
+    }, (err) => {
+      console.error('未能复制文本:', err);
+    });
+  }
+
+  insertIntoChapterEnd() {
+    // 检查是否有选择的章节
+    if (this.continuedText && this.onInsertText) {
+      this.onInsertText(this.continuedText);
+      this.dismiss();
+    } else {
+      alert('请先生成续写内容');
+    }
+  }
+
+  regenerate() {
+    // 清空输入框和结果区域
+    this.userPrompt = '';
+    this.responseMsg = '';
+    this.continuedText = '';
+    this.isComplete = false;
+    // 重新发送请求
+    this.sendMessage();
+  }
+}

+ 38 - 0
novel-app/src/app/ai-expand-modal/ai-expand-modal.component.html

@@ -0,0 +1,38 @@
+<!-- ai-expand-modal.component.html -->
+<ion-header>
+  <ion-toolbar>
+    <ion-title>AI 扩写</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="dismiss()">关闭</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <!-- 文本域:生成提示词 -->
+  <ion-textarea [value]="userPrompt" (ionInput)="promptInput($event)" placeholder="请输入想要扩写的内容~"
+    autoGrow="true"></ion-textarea>
+
+  <!-- 按钮:执行消息生成函数 -->
+  <ion-button (click)="sendMessage()" expand="block" class="primary">开始扩写</ion-button>
+
+  <!-- 展示:返回消息内容 -->
+  <!-- 消息传输过程中,实时预览 -->
+  @if(!isComplete){
+  <div>{{responseMsg}}</div>
+  }
+  <!-- 消息传输完成后,实时预览Markdown格式 -->
+  @if(isComplete){
+  <fm-markdown-preview class="content-style" [content]="responseMsg"></fm-markdown-preview>
+  <ion-textarea [(ngModel)]="expandedText" rows="10" placeholder="扩写后的内容" autoGrow="true"></ion-textarea>
+
+  <!-- 添加复制按钮 -->
+  <ion-button (click)="copyExpandedText()" class="secondary">复制</ion-button>
+
+  <!-- 添加填入文末按钮 -->
+  <ion-button (click)="insertIntoChapterEnd()" class="secondary">填入文末</ion-button>
+
+  <!-- 添加重新生成按钮 -->
+  <ion-button (click)="regenerate()" class="success">重新生成</ion-button>
+  }
+</ion-content>

+ 59 - 0
novel-app/src/app/ai-expand-modal/ai-expand-modal.component.scss

@@ -0,0 +1,59 @@
+/* ai-expand-modal.component.scss */
+
+ion-header {
+    ion-toolbar {
+        background-color: #f8f9fa;
+        color: black;
+
+        ion-title {
+            font-size: 1.5rem;
+        }
+
+        ion-buttons ion-button {
+            --color: red;
+        }
+    }
+}
+
+ion-content {
+    padding: 20px;
+
+    ion-textarea {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+
+    ion-button {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+
+        &.primary {
+            background-color: #007bff;
+            --color: white;
+        }
+
+        &.secondary {
+            background-color: #6c757d;
+            --color: white;
+        }
+
+        &.success {
+            background-color: #28a745;
+            --color: white;
+        }
+    }
+
+    .content-style {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+}

+ 24 - 0
novel-app/src/app/ai-expand-modal/ai-expand-modal.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { AiExpandModalComponent } from './ai-expand-modal.component';
+
+describe('AiExpandModalComponent', () => {
+  let component: AiExpandModalComponent;
+  let fixture: ComponentFixture<AiExpandModalComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [ AiExpandModalComponent ],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AiExpandModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 93 - 0
novel-app/src/app/ai-expand-modal/ai-expand-modal.component.ts

@@ -0,0 +1,93 @@
+// ai-expand-modal.component.ts
+import { Component, Input } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+import {
+  IonButton, IonIcon, IonInput, IonItem, IonLabel,
+  IonToolbar, IonHeader, IonContent, IonTitle, IonButtons,
+  IonTextarea
+} from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms'; // 导入 FormsModule
+import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-ai-expand-modal',
+  templateUrl: './ai-expand-modal.component.html',
+  styleUrls: ['./ai-expand-modal.component.scss'],
+  standalone: true,
+  imports: [
+    IonButton, IonIcon, IonInput, IonItem, IonTitle, FormsModule,
+    IonLabel, IonToolbar, IonHeader, IonContent, IonButtons, MarkdownPreviewModule, IonTextarea
+  ]
+})
+export class AiExpandModalComponent {
+  inputText: string = '';
+  expandedText: string = '';
+  userPrompt: string = "";
+  responseMsg: any = "";
+  isComplete: boolean = false;
+
+  @Input() onInsertText?: (text: string) => void;
+
+  constructor(private modalCtrl: ModalController) { }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+
+  promptInput(ev: any) {
+    this.userPrompt = ev.detail.value;
+  }
+
+  sendMessage() {
+    console.log("create");
+
+    let PromptTemplate = `
+    您作为一名专业的小说作者,请您根据用户输入的段落,进行扩写。
+    以下是用户的口述:${this.userPrompt}
+    `;
+
+    let completion = new FmodeChatCompletion([
+      { role: "system", content: "" },
+      { role: "user", content: PromptTemplate }
+    ]);
+
+    completion.sendCompletion().subscribe((message: any) => {
+      // 打印消息体
+      console.log(message.content);
+      // 赋值消息内容给组件内属性
+      this.responseMsg = message.content;
+      this.expandedText = message.content; // 将生成的内容赋值给 expandedText
+      if (message?.complete) { // 判断message为完成状态,则设置isComplete为完成
+        this.isComplete = true;
+      }
+    });
+  }
+
+  copyExpandedText() {
+    navigator.clipboard.writeText(this.expandedText).then(() => {
+      alert('文本已复制到剪贴板');
+    }, (err) => {
+      console.error('未能复制文本:', err);
+    });
+  }
+
+  insertIntoChapterEnd() {
+    // 检查是否有选择的章节
+    if (this.expandedText && this.onInsertText) {
+      this.onInsertText(this.expandedText);
+      this.dismiss();
+    } else {
+      alert('请先生成扩写内容');
+    }
+  }
+
+  regenerate() {
+    // 清空输入框和结果区域
+    this.userPrompt = '';
+    this.responseMsg = '';
+    this.expandedText = '';
+    this.isComplete = false;
+    // 重新发送请求
+    this.sendMessage();
+  }
+}

+ 38 - 0
novel-app/src/app/ai-polish-modal/ai-polish-modal.component.html

@@ -0,0 +1,38 @@
+<!-- ai-polish-modal.component.html -->
+<ion-header>
+  <ion-toolbar>
+    <ion-title>AI 润色</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="dismiss()">关闭</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <!-- 文本域:生成提示词 -->
+  <ion-textarea [value]="userPrompt" (ionInput)="promptInput($event)" placeholder="请输入想要润色的内容~"
+    autoGrow="true"></ion-textarea>
+
+  <!-- 按钮:执行消息生成函数 -->
+  <ion-button (click)="sendMessage()" expand="block" class="primary">开始润色</ion-button>
+
+  <!-- 展示:返回消息内容 -->
+  <!-- 消息传输过程中,实时预览 -->
+  @if(!isComplete){
+  <div>{{responseMsg}}</div>
+  }
+  <!-- 消息传输完成后,实时预览Markdown格式 -->
+  @if(isComplete){
+  <fm-markdown-preview class="content-style" [content]="responseMsg"></fm-markdown-preview>
+  <ion-textarea [(ngModel)]="polishedText" rows="10" placeholder="润色后的内容" autoGrow="true"></ion-textarea>
+
+  <!-- 添加复制按钮 -->
+  <ion-button (click)="copyPolishedText()" class="secondary">复制</ion-button>
+
+  <!-- 添加填入文末按钮 -->
+  <ion-button (click)="insertIntoChapterEnd()" class="secondary">填入文末</ion-button>
+
+  <!-- 添加重新生成按钮 -->
+  <ion-button (click)="regenerate()" class="success">重新生成</ion-button>
+  }
+</ion-content>

+ 59 - 0
novel-app/src/app/ai-polish-modal/ai-polish-modal.component.scss

@@ -0,0 +1,59 @@
+/* ai-expand-modal.component.scss */
+
+ion-header {
+    ion-toolbar {
+        background-color: #f8f9fa;
+        color: black;
+
+        ion-title {
+            font-size: 1.5rem;
+        }
+
+        ion-buttons ion-button {
+            --color: red;
+        }
+    }
+}
+
+ion-content {
+    padding: 20px;
+
+    ion-textarea {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+
+    ion-button {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+
+        &.primary {
+            background-color: #007bff;
+            --color: white;
+        }
+
+        &.secondary {
+            background-color: #6c757d;
+            --color: white;
+        }
+
+        &.success {
+            background-color: #28a745;
+            --color: white;
+        }
+    }
+
+    .content-style {
+        border-radius: 10px;
+        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border: 1px solid #ced4da;
+    }
+}

+ 24 - 0
novel-app/src/app/ai-polish-modal/ai-polish-modal.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { AiPolishModalComponent } from './ai-polish-modal.component';
+
+describe('AiPolishModalComponent', () => {
+  let component: AiPolishModalComponent;
+  let fixture: ComponentFixture<AiPolishModalComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [ AiPolishModalComponent ],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AiPolishModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 89 - 0
novel-app/src/app/ai-polish-modal/ai-polish-modal.component.ts

@@ -0,0 +1,89 @@
+// ai-polish-modal.component.ts
+import { Component, Input } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+import { IonButton, IonIcon, IonInput, IonItem, IonLabel, IonToolbar, IonHeader, IonContent, IonTitle, IonButtons, IonTextarea } from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms';
+import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-ai-polish-modal',
+  templateUrl: './ai-polish-modal.component.html',
+  styleUrls: ['./ai-polish-modal.component.scss'],
+  standalone: true,
+  imports: [
+    IonButton, IonIcon, IonInput, IonItem, IonTitle, FormsModule,
+    IonLabel, IonToolbar, IonHeader, IonContent, IonButtons, MarkdownPreviewModule, IonTextarea
+  ]
+})
+export class AiPolishModalComponent {
+  inputText: string = '';
+  polishedText: string = '';
+  userPrompt: string = "";
+  responseMsg: any = "";
+  isComplete: boolean = false;
+
+  @Input() onInsertText?: (text: string) => void;
+
+  constructor(private modalCtrl: ModalController) { }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+
+  promptInput(ev: any) {
+    this.userPrompt = ev.detail.value;
+  }
+
+  sendMessage() {
+    console.log("create");
+
+    let PromptTemplate = `
+    您是一名专业的文字润色师,请您对用户提供的文本进行润色。
+    以下是用户的原文:${this.userPrompt}
+    `;
+
+    let completion = new FmodeChatCompletion([
+      { role: "system", content: "" },
+      { role: "user", content: PromptTemplate }
+    ]);
+
+    completion.sendCompletion().subscribe((message: any) => {
+      // 打印消息体
+      console.log(message.content);
+      // 赋值消息内容给组件内属性
+      this.responseMsg = message.content;
+      this.polishedText = message.content; // 将生成的内容赋值给 polishedText
+      if (message?.complete) { // 判断message为完成状态,则设置isComplete为完成
+        this.isComplete = true;
+      }
+    });
+  }
+
+  copyPolishedText() {
+    navigator.clipboard.writeText(this.polishedText).then(() => {
+      alert('文本已复制到剪贴板');
+    }, (err) => {
+      console.error('未能复制文本:', err);
+    });
+  }
+
+  insertIntoChapterEnd() {
+    // 检查是否有选择的章节
+    if (this.polishedText && this.onInsertText) {
+      this.onInsertText(this.polishedText);
+      this.dismiss();
+    } else {
+      alert('请先生成润色内容');
+    }
+  }
+
+  regenerate() {
+    // 清空输入框和结果区域
+    this.userPrompt = '';
+    this.responseMsg = '';
+    this.polishedText = '';
+    this.isComplete = false;
+    // 重新发送请求
+    this.sendMessage();
+  }
+}

+ 1 - 1
novel-app/src/app/app.component.spec.ts

@@ -8,7 +8,7 @@ describe('AppComponent', () => {
       imports: [AppComponent],
       providers: [provideRouter([])]
     }).compileComponents();
-    
+
     const fixture = TestBed.createComponent(AppComponent);
     const app = fixture.componentInstance;
     expect(app).toBeTruthy();

+ 2 - 2
novel-app/src/app/app.routes.ts

@@ -42,10 +42,10 @@ export const routes: Routes = [
   },
   {
     path: 'chapter-generator',
-    loadComponent: () => import('./chapter-generator/chapter-generator.page').then( m => m.ChapterGeneratorPage)
+    loadComponent: () => import('./chapter-generator/chapter-generator.page').then(m => m.ChapterGeneratorPage)
   },
   {
-    path: 'character-detail/:id', 
+    path: 'character-detail/:id',
     loadComponent: () => import('./character-detail/character-detail.component').then(m => m.CharacterDetailComponent)
   }
 

+ 1 - 0
novel-app/src/app/chapter-generator/README.md

@@ -0,0 +1 @@
+# 长篇小说章节编辑

+ 11 - 3
novel-app/src/app/chapter-generator/chapter-generator.page.html

@@ -1,4 +1,5 @@
 <!-- chapter-generator.page.html -->
+
 <ion-header>
   <ion-toolbar>
     <!-- 返回箭头 -->
@@ -8,6 +9,12 @@
 
     <!-- 页面标题 -->
     <ion-title>章节编辑</ion-title>
+
+    <!-- 保存作品按钮 -->
+    <ion-buttons slot="end">
+      <ion-button (click)="saveWork()" color="primary">保存作品</ion-button>
+    </ion-buttons>
+
   </ion-toolbar>
 </ion-header>
 
@@ -42,9 +49,10 @@
 
     <!-- 内容区域 -->
     <div class="chapter-edit-container">
-      <ion-button>AI续写</ion-button>
-      <ion-button>AI润色</ion-button>
-      <ion-button>AI扩写</ion-button>
+      <ion-button (click)="openAiContinueModal()">AI续写</ion-button>
+      <ion-button (click)="openAiPolishModal()">AI润色</ion-button>
+      <ion-button (click)="openAiExpandModal()">AI扩写</ion-button>
+
       @if(selectedChapterIndex !== null){
       <ion-item>
         <ion-label position="floating">章节标题</ion-label>

+ 83 - 13
novel-app/src/app/chapter-generator/chapter-generator.page.ts

@@ -1,10 +1,15 @@
 // chapter-generator.page.ts
-import { CommonModule } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { FormsModule } from '@angular/forms'; // 导入 FormsModule
-import { IonBackButton, IonButton, IonButtons, IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonInput, IonItem, IonLabel, IonList, IonMenu, IonMenuButton, IonRouterOutlet, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+import { IonRouterOutlet, NavController } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
 import { chevronForward } from 'ionicons/icons';
+import { IonicModule, ModalController } from '@ionic/angular';
+import { CommonModule } from '@angular/common';
+import { AiExpandModalComponent } from '../ai-expand-modal/ai-expand-modal.component';
+import { AiPolishModalComponent } from '../ai-polish-modal/ai-polish-modal.component';
+import { AiContinueModalComponent } from '../ai-continue-modal/ai-continue-modal.component';
+import { ActivatedRoute } from '@angular/router';
 
 addIcons({ chevronForward });
 
@@ -15,19 +20,62 @@ addIcons({ chevronForward });
   standalone: true,
   imports: [
     FormsModule, // 添加 FormsModule
-    IonIcon, IonRouterOutlet,
-    IonList, IonFab, IonBackButton,
-    IonFabButton, CommonModule,
-    IonTitle, IonMenuButton, IonMenu,
-    IonContent, IonItem, IonInput, IonLabel, IonButton, IonButtons, IonHeader, IonToolbar
+    IonRouterOutlet, CommonModule, IonicModule, AiExpandModalComponent, AiPolishModalComponent, AiContinueModalComponent
   ]
 })
-export class ChapterGeneratorPage {
+export class ChapterGeneratorPage implements OnInit {
   chapters = [
     { title: 'Chapter 1', content: '这是第一章的内容。' },
     // 其他章节...
   ];
   isSideShow: boolean = true;
+  selectedChapterIndex: number | null = null;
+  selectedChapterTitle: string = '';
+  selectedChapterContent: string = '';
+  novelTitle: string = ''; // 初始化作品名称
+
+  constructor(
+    private modalCtrl: ModalController,
+    private navCtrl: NavController,
+    private route: ActivatedRoute // 注入 ActivatedRoute 以获取查询参数
+  ) { }
+
+  ngOnInit() {
+    // 从查询参数中获取作品名称
+    this.route.queryParams.subscribe(params => {
+      this.novelTitle = params['title'] || '';
+    });
+  }
+
+  async openAiExpandModal() {
+    const modal = await this.modalCtrl.create({
+      component: AiExpandModalComponent,
+      componentProps: {
+        onInsertText: (text: string) => this.insertTextIntoChapterEnd(text)
+      }
+    });
+    return await modal.present();
+  }
+
+  async openAiPolishModal() {
+    const modal = await this.modalCtrl.create({
+      component: AiPolishModalComponent,
+      componentProps: {
+        onInsertText: (text: string) => this.insertTextIntoChapterEnd(text)
+      }
+    });
+    return await modal.present();
+  }
+
+  async openAiContinueModal() {
+    const modal = await this.modalCtrl.create({
+      component: AiContinueModalComponent,
+      componentProps: {
+        onInsertText: (text: string) => this.insertTextIntoChapterEnd(text)
+      }
+    });
+    return await modal.present();
+  }
 
   toggleSide() {
     this.isSideShow = !this.isSideShow;
@@ -48,10 +96,6 @@ export class ChapterGeneratorPage {
     this.chapters.splice(index, 1);
   }
 
-  selectedChapterIndex: number | null = null;
-  selectedChapterTitle: string = '';
-  selectedChapterContent: string = '';
-
   selectChapter(index: number) {
     this.editChapter(index);
     this.isSideShow = false; // 隐藏侧边栏
@@ -74,4 +118,30 @@ export class ChapterGeneratorPage {
       this.isSideShow = true; // 显示侧边栏
     }
   }
+
+  insertTextIntoChapterEnd(text: string) {
+    if (this.selectedChapterIndex !== null) {
+      this.chapters[this.selectedChapterIndex].content += text;
+      this.selectedChapterContent += text; // 更新显示的内容
+    }
+  }
+
+  saveWork() {
+    const work = {
+      id: Date.now().toString(), // 使用当前时间戳作为唯一ID
+      cover: 'default-cover.jpg', // 默认封面,可以根据需要修改
+      title: this.novelTitle, // 使用从查询参数获取的作品名称
+      createTime: new Date(),
+      type: '长篇小说',
+      chapters: this.chapters,
+    };
+
+    // 将作品信息存储在本地存储
+    const existingWorks = JSON.parse(localStorage.getItem('works') || '[]');
+    existingWorks.push(work);
+    localStorage.setItem('works', JSON.stringify(existingWorks));
+
+    // 跳转到home页面
+    this.navCtrl.navigateRoot(['/']);
+  }
 }

+ 1 - 1
novel-app/src/app/character/character.page.ts

@@ -19,7 +19,7 @@ export class CharacterPage implements OnInit {
   constructor(
     private router: Router,
     private characterService: CharacterService
-  ) {}
+  ) { }
 
   ngOnInit() {
     this.loadCharacters();

+ 22 - 13
novel-app/src/app/home/home.page.html

@@ -57,19 +57,28 @@
 
 
       <ion-card-content>
-        <ion-grid *ngIf="stories.length > 0">
-          <ion-row>
-            <ion-col size="6" *ngFor="let story of stories">
-              <div class="story-card">
-                <ion-img [src]="story.cover" alt="story cover"></ion-img>
-                <div class="story-info">
-                  <h3>{{story.title}}</h3>
-                  <p>{{formatDate(story.createTime)}}</p>
-                </div>
-              </div>
-            </ion-col>
-          </ion-row>
-        </ion-grid>
+        <ion-list *ngIf="stories.length > 0">
+          <ion-item *ngFor="let story of stories; let i = index">
+            <ion-label>
+              <h2>{{story.title}}</h2>
+              <p>{{story.type}} | {{formatDate(story.createTime)}} | {{story.wordCount}}字</p>
+            </ion-label>
+            <ion-row>
+              <ion-col size="6">
+                <ion-button fill="clear" size="small" (click)="editWork(story.id)">
+                  <ion-icon slot="start" name="create"></ion-icon>
+                  编辑
+                </ion-button>
+              </ion-col>
+              <ion-col size="6">
+                <ion-button fill="clear" size="small" (click)="deleteWork(i, story.id)">
+                  <ion-icon slot="start" name="trash"></ion-icon>
+                  删除
+                </ion-button>
+              </ion-col>
+            </ion-row>
+          </ion-item>
+        </ion-list>
 
         <div class="empty-state" *ngIf="stories.length === 0">
           <ion-icon name="document-text-outline"></ion-icon>

+ 8 - 13
novel-app/src/app/home/home.page.scss

@@ -93,21 +93,12 @@
         }
     }
 
-    .story-card {
-        background: white;
-        border-radius: 16px;
-        overflow: hidden;
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-
-        ion-img {
-            aspect-ratio: 3/4;
-            object-fit: cover;
-        }
-
-        .story-info {
+    ion-list {
+        ion-item {
             padding: 12px;
+            border-bottom: 1px solid #ddd;
 
-            h3 {
+            h2 {
                 margin: 0;
                 font-size: 14px;
                 font-weight: 500;
@@ -119,6 +110,10 @@
                 font-size: 12px;
                 color: #666;
             }
+
+            ion-row {
+                margin-top: 8px;
+            }
         }
     }
 

+ 54 - 5
novel-app/src/app/home/home.page.ts

@@ -1,13 +1,18 @@
-import { Component, ViewChild } from '@angular/core';
+// home.page.ts
+import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { IonicModule, PopoverController } from '@ionic/angular';
+import { IonicModule, PopoverController, AlertController } from '@ionic/angular'; // 确保导入 AlertController
 import { Router } from '@angular/router';
+import { NavController } from '@ionic/angular';
 
 interface Story {
   id: string;
   cover: string;
   title: string;
   createTime: Date;
+  wordCount: number;
+  type: 'long' | 'short';
+  chapters: { title: string, content: string }[];
 }
 
 @Component({
@@ -17,14 +22,27 @@ interface Story {
   standalone: true,
   imports: [IonicModule, CommonModule]
 })
-export class HomePage {
+export class HomePage implements OnInit {
   stories: Story[] = [];
 
   constructor(
     private router: Router,
-    private popoverController: PopoverController
+    private popoverController: PopoverController,
+    private alertController: AlertController, // 确保在这里注入 AlertController
+    private navCtrl: NavController
   ) { }
 
+  ngOnInit() {
+    // 从本地存储获取作品信息
+    const savedWorks = localStorage.getItem('works');
+    if (savedWorks) {
+      this.stories = JSON.parse(savedWorks).map((story: { createTime: string | number | Date; chapters: { title: string, content: string }[] }) => ({
+        ...story,
+        createTime: new Date(story.createTime)
+      }));
+    }
+  }
+
   navigateTo(path: string) {
     this.router.navigate([path]);
   }
@@ -54,6 +72,37 @@ export class HomePage {
     this.router.navigate([targetPath]);
   }
 
+  editWork(workId: string) {
+    // 找到对应的作品并存储在本地存储
+    const work = this.stories.find(story => story.id === workId);
+    if (work) {
+      localStorage.setItem('currentWork', JSON.stringify(work));
+      this.navCtrl.navigateRoot(['/chapter-generator']);
+    }
+  }
+
+  async deleteWork(index: number, workId: string) {
+    const alert = await this.alertController.create({
+      header: '确认删除?',
+      message: '确定要删除该作品吗?',
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '删除',
+          handler: () => {
+            this.stories.splice(index, 1);
+            localStorage.setItem('works', JSON.stringify(this.stories));
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
   formatDate(date: Date): string {
     return date.toLocaleDateString('zh-CN');
   }
@@ -83,4 +132,4 @@ export class CreateOptionsComponent {
       type: type
     });
   }
-}
+}

+ 36 - 0
novel-app/src/app/services/story.service.ts

@@ -0,0 +1,36 @@
+// 假设文件路径为: d:\workspace\20222670105\novel-app\src\app\services\story.service.ts
+
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+@Injectable({
+    providedIn: 'root'
+})
+export class StoryService {
+    private storiesSource = new BehaviorSubject<any[]>([]);
+    public stories$ = this.storiesSource.asObservable();
+
+    getStories(): any[] {
+        return this.storiesSource.getValue();
+    }
+
+    setStories(stories: any[]): void {
+        this.storiesSource.next(stories);
+    }
+
+    generateUniqueId(): string {
+        return Math.random().toString(36).substr(2, 9);
+    }
+
+    addStory(story: any): Promise<void> {
+        return new Promise((resolve, reject) => {
+            setTimeout(() => {
+                const currentStories = this.getStories();
+                currentStories.push(story);
+                this.setStories(currentStories);
+                console.log('故事已添加:', story);
+                resolve();
+            }, 1000);
+        });
+    }
+}

+ 4 - 8
novel-app/src/app/short-generator/short-generator.page.html

@@ -13,16 +13,12 @@
   <ion-input [value]="title" (ionInput)="titleInput($event)"></ion-input>
   <h1>风格</h1>
   <ion-input [value]="style" (ionInput)="styleInput($event)"></ion-input>
-  
+
   <!-- 词条列表编辑区域 -->
-   <span (click)="showEntryList()">
-    当前词条 数量{{entryList?.length}}
-   </span>
+  <span (click)="showEntryList()">
+    当前词条 数量{{entryList.length}}
+  </span>
   <comp-word-entry [entryList]="entryList" (entryListChange)="onEntryListChange($event)"></comp-word-entry>
-  <!-- 文本域:生成提示词 -->
-  <h1>人物词条</h1>
-  <ion-textarea [value]="userPrompt" (ionInput)="promptInput($event)" placeholder="文本提示词"
-    autoGrow="true"></ion-textarea>
 
   <!-- 按钮:执行消息生成函数 -->
   <ion-button (click)="sendMessage()" expand="block">生成大纲</ion-button>

+ 8 - 12
novel-app/src/app/short-generator/short-generator.page.ts

@@ -37,17 +37,13 @@ export class ShortGeneratorPage implements OnInit {
     this.style = ev.detail.value;
   }
 
-  // 用户输入提示词
-  userPrompt: string = "人物名字:紫霄月;  人物角色:女主角;人物描述:一位来自古老时空的神秘女子。她自幼便拥有穿梭时空的奇异能力,能够自由穿梭于不同的历史与未来之间。这种能力不仅赋予了她非凡的见识与智慧,也让她背负上了沉重的使命——寻找失落的时空碎片,以维护时空的平衡与稳定。";
-  promptInput(ev: any) {
-    this.userPrompt = ev.detail.value;
-  }
+
   // 人物词条
-  entryList:Array<any> = []
-  onEntryListChange(ev:any){
-    this.entryList = ev
+  entryList: Array<any> = []
+  onEntryListChange(ev: any) {
+    this.entryList = ev;
   }
-  showEntryList(){
+  showEntryList() {
     console.log(JSON.stringify(this.entryList))
   }
   // 生成的小说大纲
@@ -66,8 +62,8 @@ export class ShortGeneratorPage implements OnInit {
     console.log("create");
 
     let PromptTemplate = `
-    您作为一名专业的${this.style}作者,请您根据用户提供的标题${this.title},给出短篇小说大纲。
-    以下是用户的口述:${this.userPrompt}
+    您作为一名专业的${this.style}作者,请您根据用户提供的标题${this.title},根据添加的词条${this.entryList},给出短篇小说大纲。
+    
     `;
 
     let completion = new FmodeChatCompletion([
@@ -118,7 +114,7 @@ export class ShortGeneratorPage implements OnInit {
       const novelData = {
         title: this.title,
         style: this.style,
-        userPrompt: this.userPrompt,
+        entryList: this.entryList,
         generatedOutline: this.generatedOutline,
         generatedContent: this.generatedContent
       };

+ 14 - 14
novel-app/src/app/story-generator/story-generator.page.html

@@ -8,19 +8,19 @@
 </ion-header>
 
 <ion-content class="ion-padding">
-  <ion-label position="stacked">作品名称 *</ion-label>
-  <ion-item>
-    <ion-textarea [(ngModel)]="title" placeholder="请输入作品名称"></ion-textarea>
-  </ion-item>
+  <form #storyForm="ngForm" (ngSubmit)="nextStep()">
+    <ion-item>
+      <ion-label position="stacked">作品名称 *</ion-label>
+      <ion-textarea [(ngModel)]="title" name="title" required placeholder="请输入作品名称"></ion-textarea>
+      <div *ngIf="storyForm.submitted && storyForm.controls['title'].invalid" class="error">
+        作品名称是必填项。
+      </div>
+    </ion-item>
+    <ion-label position="stacked">作品简介(选填)</ion-label>
+    <ion-item>
+      <ion-textarea [(ngModel)]="description" rows="5" placeholder="请输入作品简介"></ion-textarea>
+    </ion-item>
+    <ion-button type="submit" expand="block" [disabled]="storyForm.invalid">下一步</ion-button>
+  </form>
 
-  <ion-label position="stacked">作品简介(选填)</ion-label>
-  <ion-item>
-    <ion-textarea [(ngModel)]="description" rows="5" placeholder="请输入作品简介"></ion-textarea>
-  </ion-item>
-
-  <div class="button-container">
-    <ion-button (click)="nextStep()" expand="block" color="primary">
-      下一步
-    </ion-button>
-  </div>
 </ion-content>

+ 10 - 8
novel-app/src/app/story-generator/story-generator.page.ts

@@ -1,3 +1,4 @@
+// story-generator.page.ts
 import { Component } from '@angular/core';
 import { IonicModule } from '@ionic/angular';
 import { FormsModule } from '@angular/forms';
@@ -13,15 +14,16 @@ import { Router } from '@angular/router';
 export class StoryGeneratorPage {
   title: string = '';
   description: string = '';
-  constructor(
-    private router: Router,
-  ) { }
+
+  constructor(private router: Router) { }
 
   nextStep() {
-    // 处理下一步逻辑
-    console.log('Title:', this.title);
-    console.log('Description:', this.description);
-    // 导航到 chapter-generator 页面
-    this.router.navigate(['/chapter-generator']);
+    if (this.title.trim() === '') {
+      console.log('作品名称是必填项');
+      return;
+    }
+
+    // 导航到 chapter-generator 页面并传递作品名称
+    this.router.navigate(['/chapter-generator'], { queryParams: { title: this.title } });
   }
 }