Prechádzať zdrojové kódy

Merge pull request #2270 from visuddhinanda/development

Development
visuddhinanda 11 mesiacov pred
rodič
commit
76f62a750c

+ 209 - 160
api-v8/app/Console/Commands/MqAiTranslate.php

@@ -53,192 +53,241 @@ class MqAiTranslate extends Command
         $queue = 'ai_translate';
         $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
         Log::debug("mq worker {$queue} start.");
-        Mq::worker($exchange, $queue, function ($message) use ($queue) {
-            Log::debug('ai translate start', ['message' => $message]);
-            //写入 model log
-            $modelLog = new ModelLog();
-            $modelLog->uid = Str::uuid();
-
-            $param = [
-                "model" => $message->model->model,
-                "messages" => [
-                    ["role" => "system", "content" => $message->model->system_prompt],
-                    ["role" => "user", "content" => $message->prompt],
-                ],
-                'prompt' => $message->prompt,
-                "temperature" => 0.7,
-                "stream" => false
+        Mq::worker($exchange, $queue, function ($messages) use ($queue) {
+            Log::debug('ai translate start', ['message' => count($messages)]);
+            $this->info('ai translate task start task=' . count($messages));
+            if (!is_array($messages) || count($messages) === 0) {
+                Log::error('message is not array');
+                return 1;
+            }
+            //获取model token
+            $first = $messages[0];
+            Log::debug($queue . ' ai assistant token', ['user' => $first->model->uid]);
+            $modelToken = AuthController::getUserToken($first->model->uid);
+            Log::debug($queue . ' ai assistant token', ['token' => $modelToken]);
+            $discussionUrl = config('app.url') . '/api/v2/discussion';
+            $taskDiscussionData = [
+                'res_id' => $first->task->info->id,
+                'res_type' => 'task',
+                'title' => $first->task->info->title,
+                'content' => $first->task->info->category,
+                'content_type' => 'markdown',
+                'type' => 'discussion',
+                'notification' => false,
             ];
-            Log::info($queue . ' LLM request' . $message->model->url);
-            Log::info($queue . ' model:' . $param['model']);
-            Log::debug($queue . ' LLM api request', [
-                'url' => $message->model->url,
-                'data' => $param
-            ]);
-            $modelLog->model_id = $message->model->uid;
-            $modelLog->request_at = now();
-            $modelLog->request_data = json_encode($param, JSON_UNESCAPED_UNICODE);
-            try {
-                $response = Http::withToken($message->model->key)
-                    ->timeout(300)
-                    ->post($message->model->url, $param);
-
-                $response->throw(); // 触发异常(如果请求失败)
-
-                Log::info($queue . ' LLM request successful');
-                $modelLog->request_headers = json_encode($response->handlerStats(), JSON_UNESCAPED_UNICODE);
-                $modelLog->response_headers = json_encode($response->headers(), JSON_UNESCAPED_UNICODE);
-                $modelLog->status = $response->status();
-                $modelLog->response_data = json_encode($response->json(), JSON_UNESCAPED_UNICODE);
-                /*
+            $response = Http::timeout(10)->withToken($modelToken)->post($discussionUrl, $taskDiscussionData);
+            if ($response->failed()) {
+                Log::error($queue . ' discussion create topic error', ['data' => $response->json()]);
+            } else {
+                if (isset($response->json()['data']['id'])) {
+                    $taskDiscussionData['parent'] = $response->json()['data']['id'];
+                }
+            }
+
+
+            foreach ($messages as $key => $message) {
+                $taskDiscussionContent = [];
+                //写入 model log
+                $modelLog = new ModelLog();
+                $modelLog->uid = Str::uuid();
+
+                $param = [
+                    "model" => $message->model->model,
+                    "messages" => [
+                        ["role" => "system", "content" => $message->model->system_prompt ?? ''],
+                        ["role" => "user", "content" => $message->prompt],
+                    ],
+                    'prompt' => $message->prompt,
+                    "temperature" => 0.7,
+                    "stream" => false
+                ];
+                Log::info($queue . ' LLM request' . $message->model->url);
+                Log::info($queue . ' model:' . $param['model']);
+                Log::debug($queue . ' LLM api request', [
+                    'url' => $message->model->url,
+                    'data' => $param
+                ]);
+                $modelLog->model_id = $message->model->uid;
+                $modelLog->request_at = now();
+                $modelLog->request_data = json_encode($param, JSON_UNESCAPED_UNICODE);
+                try {
+                    $response = Http::withToken($message->model->key)
+                        ->timeout(300)
+                        ->post($message->model->url, $param);
+
+                    $response->throw(); // 触发异常(如果请求失败)
+                    $taskDiscussionContent[] = '- LLM request successful';
+                    Log::info($queue . ' LLM request successful');
+                    $modelLog->request_headers = json_encode($response->handlerStats(), JSON_UNESCAPED_UNICODE);
+                    $modelLog->response_headers = json_encode($response->headers(), JSON_UNESCAPED_UNICODE);
+                    $modelLog->status = $response->status();
+                    $modelLog->response_data = json_encode($response->json(), JSON_UNESCAPED_UNICODE);
+                    /*
                 if ($response->failed()) {
                     $modelLog->success = false;
                     $modelLog->save();
                     Log::error($queue . ' http response error', ['data' => $response->json()]);
                     return 1;
                 }*/
-            } catch (RequestException $e) {
-                Log::error($queue . ' LLM request exception: ' . $e->getMessage());
-                $failResponse = $e->response;
-                $modelLog->request_headers = json_encode($failResponse->handlerStats(), JSON_UNESCAPED_UNICODE);
-                $modelLog->response_headers = json_encode($failResponse->headers(), JSON_UNESCAPED_UNICODE);
-                $modelLog->status = $failResponse->status();
-                $modelLog->response_data = $response->body();
-                $modelLog->success = false;
-                $modelLog->save();
-                return 1;
-            }
-
+                } catch (RequestException $e) {
+                    Log::error($queue . ' LLM request exception: ' . $e->getMessage());
+                    $failResponse = $e->response;
+                    $modelLog->request_headers = json_encode($failResponse->handlerStats(), JSON_UNESCAPED_UNICODE);
+                    $modelLog->response_headers = json_encode($failResponse->headers(), JSON_UNESCAPED_UNICODE);
+                    $modelLog->status = $failResponse->status();
+                    $modelLog->response_data = $response->body();
+                    $modelLog->success = false;
+                    $modelLog->save();
+                    continue;
+                }
 
-            $modelLog->save();
-            Log::info($queue . ' model log saved');
-            $aiData = $response->json();
-            Log::debug($queue . ' LLM http response', ['data' => $response->json()]);
-            $responseContent = $aiData['choices'][0]['message']['content'];
-            if (isset($aiData['choices'][0]['message']['reasoning_content'])) {
-                $reasoningContent = $aiData['choices'][0]['message']['reasoning_content'];
-            }
 
-            Log::debug($queue . ' LLM response content=' . $responseContent);
-            if (empty($reasoningContent)) {
-                Log::debug($queue . ' no reasoningContent');
-            } else {
-                Log::debug($queue . ' reasoning=' . $reasoningContent);
-            }
+                $modelLog->save();
+                Log::info($queue . ' model log saved');
+                $aiData = $response->json();
+                Log::debug($queue . ' LLM http response', ['data' => $response->json()]);
+                $responseContent = $aiData['choices'][0]['message']['content'];
+                if (isset($aiData['choices'][0]['message']['reasoning_content'])) {
+                    $reasoningContent = $aiData['choices'][0]['message']['reasoning_content'];
+                }
 
-            //获取model token
-            Log::debug($queue . ' ai assistant token', ['user' => $message->model->uid]);
-            $token = AuthController::getUserToken($message->model->uid);
-            Log::debug($queue . ' ai assistant token', ['token' => $token]);
-
-            if ($message->task->info->category === 'translate') {
-                //写入句子库
-                $url = config('app.url') . '/api/v2/sentence';
-                $sentData = [];
-                $message->sentence->content = $responseContent;
-                $sentData[] = $message->sentence;
-                Log::info($queue . " sentence update {$url}");
-                $response = Http::timeout(10)->withToken($token)->post($url, [
-                    'sentences' => $sentData,
-                ]);
-                if ($response->failed()) {
-                    Log::error($queue . ' sentence update failed', [
-                        'url' => $url,
-                        'data' => $response->json(),
-                    ]);
-                    return 1;
+                Log::debug($queue . ' LLM response content=' . $responseContent);
+                if (empty($reasoningContent)) {
+                    Log::debug($queue . ' no reasoningContent');
                 } else {
-                    $count = $response->json()['data']['count'];
-                    Log::info("{$queue} sentence update {$count} successful");
+                    Log::debug($queue . ' reasoning=' . $reasoningContent);
                 }
-            }
 
-            //写入discussion
-            #获取句子id
-            $sUid = Sentence::where('book_id', $message->sentence->book_id)
-                ->where('paragraph', $message->sentence->paragraph)
-                ->where('word_start', $message->sentence->word_start)
-                ->where('word_end', $message->sentence->word_end)
-                ->where('channel_uid', $message->sentence->channel_uid)
-                ->value('uid');
-            $url = config('app.url') . '/api/v2/discussion';
-            $data = [
-                'res_id' => $sUid,
-                'res_type' => 'sentence',
-                'title' => $message->task->info->title,
-                'content' => $message->task->info->category,
-                'content_type' => 'markdown',
-                'type' => 'discussion',
-                'notification' => false,
-            ];
-            $response = Http::timeout(10)->withToken($token)->post($url, $data);
-            if ($response->failed()) {
-                Log::error($queue . ' discussion create topic error', ['data' => $response->json()]);
-            } else {
-                if (isset($response->json()['data']['id'])) {
-                    Log::info($queue . ' discussion create topic successful');
-                    $data['parent'] = $response->json()['data']['id'];
-                    unset($data['title']);
-                    $topicChildren = [];
-                    //提示词
-                    $topicChildren[] = $message->prompt;
-                    //任务结果
-                    $topicChildren[] = $responseContent;
-                    //推理过程写入discussion
-                    if (isset($reasoningContent) && !empty($reasoningContent)) {
-                        $topicChildren[] = $reasoningContent;
+                //获取model token
+                Log::debug($queue . ' ai assistant token', ['user' => $message->model->uid]);
+                $token = AuthController::getUserToken($message->model->uid);
+                Log::debug($queue . ' ai assistant token', ['token' => $token]);
+
+                if ($message->task->info->category === 'translate') {
+                    //写入句子库
+                    $url = config('app.url') . '/api/v2/sentence';
+                    $sentData = [];
+                    $message->sentence->content = $responseContent;
+                    $sentData[] = $message->sentence;
+                    Log::info($queue . " sentence update {$url}");
+                    $response = Http::timeout(10)->withToken($token)->post($url, [
+                        'sentences' => $sentData,
+                    ]);
+                    if ($response->failed()) {
+                        Log::error($queue . ' sentence update failed', [
+                            'url' => $url,
+                            'data' => $response->json(),
+                        ]);
+                        continue;
+                    } else {
+                        $count = $response->json()['data']['count'];
+                        Log::info("{$queue} sentence update {$count} successful");
                     }
-                    foreach ($topicChildren as  $content) {
-                        $data['content'] = $content;
-                        Log::debug($queue . ' discussion child request', ['url' => $url, 'data' => $data]);
-                        $response = Http::timeout(10)->withToken($token)->post($url, $data);
-                        if ($response->failed()) {
-                            Log::error($queue . ' discussion error', ['data' => $response->json()]);
-                        } else {
-                            Log::info($queue . ' discussion child successful');
+                }
+
+                //写入discussion
+                #获取句子id
+                $sUid = Sentence::where('book_id', $message->sentence->book_id)
+                    ->where('paragraph', $message->sentence->paragraph)
+                    ->where('word_start', $message->sentence->word_start)
+                    ->where('word_end', $message->sentence->word_end)
+                    ->where('channel_uid', $message->sentence->channel_uid)
+                    ->value('uid');
+                $url = config('app.url') . '/api/v2/discussion';
+                $data = [
+                    'res_id' => $sUid,
+                    'res_type' => 'sentence',
+                    'title' => $message->task->info->title,
+                    'content' => $message->task->info->category,
+                    'content_type' => 'markdown',
+                    'type' => 'discussion',
+                    'notification' => false,
+                ];
+                $response = Http::timeout(10)->withToken($token)->post($url, $data);
+                if ($response->failed()) {
+                    Log::error($queue . ' discussion create topic error', ['data' => $response->json()]);
+                } else {
+                    if (isset($response->json()['data']['id'])) {
+                        Log::info($queue . ' discussion create topic successful');
+                        $data['parent'] = $response->json()['data']['id'];
+                        unset($data['title']);
+                        $topicChildren = [];
+                        //提示词
+                        $topicChildren[] = $message->prompt;
+                        //任务结果
+                        $topicChildren[] = $responseContent;
+                        //推理过程写入discussion
+                        if (isset($reasoningContent) && !empty($reasoningContent)) {
+                            $topicChildren[] = $reasoningContent;
                         }
+                        foreach ($topicChildren as  $content) {
+                            $data['content'] = $content;
+                            Log::debug($queue . ' discussion child request', ['url' => $url, 'data' => $data]);
+                            $response = Http::timeout(10)->withToken($token)->post($url, $data);
+                            if ($response->failed()) {
+                                Log::error($queue . ' discussion error', ['data' => $response->json()]);
+                            } else {
+                                Log::info($queue . ' discussion child successful');
+                            }
+                        }
+                    } else {
+                        Log::error($queue . ' discussion create topic response is null');
                     }
-                } else {
-                    Log::error($queue . ' discussion create topic response is null');
                 }
-            }
 
 
-            //修改task 完成度
-            $taskProgress = $message->task->progress;
-            if ($taskProgress->total > 0) {
-                $progress = (int)($taskProgress->current * 100 / $taskProgress->total);
-            } else {
-                $progress = 100;
-                Log::error($queue . ' progress total is zero', ['task_id' => $message->task->info->id]);
-            }
-            $url = config('app.url') . '/api/v2/task/' . $message->task->info->id;
-            $data = [
-                'progress' => $progress,
-            ];
-            Log::debug($queue . ' task progress request', ['url' => $url, 'data' => $data]);
-            $response = Http::timeout(10)->withToken($token)->patch($url, $data);
-            if ($response->failed()) {
-                Log::error($queue . ' task progress error', ['data' => $response->json()]);
-            } else {
-                Log::info($queue . ' task progress successful progress=' . $response->json()['data']['progress']);
-            }
-
-            //任务完成 修改任务状态为 done
-            if ($progress === 100) {
-                $url = config('app.url') . '/api/v2/task-status/' . $message->task->info->id;
+                //修改task 完成度
+                $taskProgress = $message->task->progress;
+                if ($taskProgress->total > 0) {
+                    $progress = (int)($taskProgress->current * 100 / $taskProgress->total);
+                } else {
+                    $progress = 100;
+                    Log::error($queue . ' progress total is zero', ['task_id' => $message->task->info->id]);
+                }
+                $url = config('app.url') . '/api/v2/task/' . $message->task->info->id;
                 $data = [
-                    'status' => 'done',
+                    'progress' => $progress,
                 ];
-                Log::debug($queue . ' task status request', ['url' => $url, 'data' => $data]);
+                Log::debug($queue . ' task progress request', ['url' => $url, 'data' => $data]);
                 $response = Http::timeout(10)->withToken($token)->patch($url, $data);
-                //判断状态码
                 if ($response->failed()) {
-                    Log::error($queue . ' task status error', ['data' => $response->json()]);
+                    Log::error($queue . ' task progress error', ['data' => $response->json()]);
+                } else {
+                    $taskDiscussionContent[] = "- progress=" . $response->json()['data']['progress'];
+                    Log::info($queue . ' task progress successful progress=' . $response->json()['data']['progress']);
+                }
+
+                if (isset($taskDiscussionData['parent'])) {
+                    unset($taskDiscussionData['title']);
+                    $taskDiscussionData['content'] = implode('\n', $taskDiscussionContent);
+                    Log::debug($queue . ' task discussion child request', ['url' => $discussionUrl, 'data' => $data]);
+                    $response = Http::timeout(10)->withToken($token)->post($discussionUrl, $taskDiscussionData);
+                    if ($response->failed()) {
+                        Log::error($queue . ' task discussion error', ['data' => $response->json()]);
+                    } else {
+                        Log::info($queue . ' task discussion child successful');
+                    }
                 } else {
-                    Log::info($queue . ' task status done');
+                    Log::error('no task discussion root');
+                }
+
+                //任务完成 修改任务状态为 done
+                if ($progress === 100) {
+                    $url = config('app.url') . '/api/v2/task-status/' . $message->task->info->id;
+                    $data = [
+                        'status' => 'done',
+                    ];
+                    Log::debug($queue . ' task status request', ['url' => $url, 'data' => $data]);
+                    $response = Http::timeout(10)->withToken($token)->patch($url, $data);
+                    //判断状态码
+                    if ($response->failed()) {
+                        Log::error($queue . ' task status error', ['data' => $response->json()]);
+                    } else {
+                        Log::info($queue . ' task status done');
+                    }
                 }
             }
+            $this->info('ai translate task complete');
             return 0;
         });
         return 0;

+ 61 - 0
api-v8/app/Console/Commands/MqEmpty.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\Mq;
+
+class MqEmpty extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'mq:empty';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if (\App\Tools\Tools::isStop()) {
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'ai_translate';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq worker {$queue} start.");
+        Mq::worker(
+            $exchange,
+            $queue,
+            function ($message) {
+                $this->info('new message');
+                sleep(3);
+                return 0;
+            }
+        );
+
+        return 0;
+    }
+}

+ 5 - 3
api-v8/app/Console/Commands/TestAiTask.php

@@ -10,10 +10,10 @@ class TestAiTask extends Command
     /**
      * The name and signature of the console command.
      * php artisan test:ai.task c77af42f-ffb5-48ae-af71-4c32e1c30dab
-     * php artisan test:ai.task 41640b40-e153-407d-9d29-da631c5b88f8
+     * php artisan test:ai.task f42fa690-c590-400f-9de9-fbc81e838a5a
      * @var string
      */
-    protected $signature = 'test:ai.task {id}';
+    protected $signature = 'test:ai.task {id} {--test}';
 
     /**
      * The console command description.
@@ -40,8 +40,10 @@ class TestAiTask extends Command
     public function handle()
     {
         $taskId = $this->argument('id');
-        $params = AiTaskPrepare::translate($taskId, false);
+        $params = AiTaskPrepare::translate($taskId, !$this->option('test'));
         var_dump($params);
+        var_dump($this->option('test'));
+        $this->info('total:' . count($params));
         return 0;
     }
 }

+ 46 - 0
api-v8/app/Console/Commands/TestMqExit.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+
+class TestMqExit extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan test:mq.exit
+     * @var string
+     */
+    protected $signature = 'test:mq.exit';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        for ($i = 0; $i < 10; $i++) {
+            Mq::publish('ai_translate', ['hello world']);
+        }
+        return 0;
+    }
+}

+ 2 - 0
api-v8/app/Http/Api/AiAssistantApi.php

@@ -42,6 +42,8 @@ class AiAssistantApi
             } else {
                 $data['avatar'] = Storage::temporaryUrl($img, now()->addDays(6));
             }
+        } else {
+            $data['avatar'] = config('app.url') . '/assets/images/avatar/ai-assistant.png';
         }
         return $data;
     }

+ 3 - 3
api-v8/app/Http/Api/AiTaskPrepare.php

@@ -173,9 +173,9 @@ class AiTaskPrepare
                 ],
             ];
             array_push($mqData, $aiMqData);
-            if ($send) {
-                Mq::publish('ai_translate', $aiMqData);
-            }
+        }
+        if ($send) {
+            Mq::publish('ai_translate', $mqData);
         }
         return $mqData;
     }

+ 2 - 2
api-v8/app/Http/Api/Mq.php

@@ -158,7 +158,7 @@ class Mq
                 }
 
                 if (\App\Tools\Tools::isStop()) {
-                    Log::debug('mq worker: .stop file exist. cancel the consumer.');
+                    Log::info('mq worker: .stop file exist. cancel the consumer.');
                     $message->getChannel()->basic_cancel($message->getConsumerTag());
                 }
             }
@@ -174,7 +174,7 @@ class Mq
                             $GLOBALS[$key] = 1;
                         }
                         if ($GLOBALS[$key] >= $value) {
-                            Log::debug("mq exit queue={$queue} loop=" . $GLOBALS[$key]);
+                            Log::info("mq exit queue={$queue} loop=" . $GLOBALS[$key]);
                             $message->getChannel()->basic_cancel($message->getConsumerTag());
                         }
                     }

BIN
api-v8/public/assets/images/avatar/ai-assistant.png


+ 31 - 25
dashboard-v4/dashboard/src/components/discussion/DiscussionListCard.tsx

@@ -16,7 +16,7 @@ import { currentUser as _currentUser } from "../../reducers/current-user";
 import { CommentOutlinedIcon, TemplateOutlinedIcon } from "../../assets/icon";
 import { ISentenceResponse } from "../api/Corpus";
 import { TDiscussionType } from "./Discussion";
-import { courseInfo, memberInfo } from "../../reducers/current-course";
+import { courseInfo } from "../../reducers/current-course";
 import { courseUser } from "../../reducers/course-user";
 import TimeShow from "../general/TimeShow";
 
@@ -26,7 +26,8 @@ export type TResType =
   | "chapter"
   | "sentence"
   | "wbw"
-  | "term";
+  | "term"
+  | "task";
 
 interface IWidget {
   resId?: string;
@@ -64,7 +65,6 @@ const DiscussionListCardWidget = ({
   const [canCreate, setCanCreate] = useState(false);
 
   const course = useAppSelector(courseInfo);
-  const courseMember = useAppSelector(memberInfo);
   const myCourse = useAppSelector(courseUser);
 
   const user = useAppSelector(_currentUser);
@@ -108,20 +108,32 @@ const DiscussionListCardWidget = ({
             render(dom, entity, index, action, schema) {
               return (
                 <>
-                  {entity.resId !== resId ? <LinkOutlined /> : <></>}
-                  <Button
-                    key={index}
-                    size="small"
-                    type="link"
-                    icon={entity.newTpl ? <TemplateOutlinedIcon /> : undefined}
-                    onClick={(event) => {
-                      if (typeof onSelect !== "undefined") {
-                        onSelect(event, entity);
+                  <div>
+                    {entity.resId !== resId ? <LinkOutlined /> : <></>}
+                    <Button
+                      key={index}
+                      size="small"
+                      type="link"
+                      icon={
+                        entity.newTpl ? <TemplateOutlinedIcon /> : undefined
                       }
-                    }}
-                  >
-                    {entity.title}
-                  </Button>
+                      onClick={(event) => {
+                        if (typeof onSelect !== "undefined") {
+                          onSelect(event, entity);
+                        }
+                      }}
+                    >
+                      {entity.title}
+                    </Button>
+                  </div>
+                  <Space>
+                    <TimeShow
+                      type="secondary"
+                      showIcon={false}
+                      createdAt={entity.createdAt}
+                      updatedAt={entity.updatedAt}
+                    />
+                  </Space>
                 </>
               );
             },
@@ -132,15 +144,9 @@ const DiscussionListCardWidget = ({
             render(dom, entity, index, action, schema) {
               return (
                 <div>
-                  <div key={index}>{entity.summary ?? entity.content}</div>
-                  <Space>
-                    {entity.user.nickName}
-                    <TimeShow
-                      type="secondary"
-                      createdAt={entity.createdAt}
-                      updatedAt={entity.updatedAt}
-                    />
-                  </Space>
+                  <div key={index}>
+                    {entity.summary ?? entity.content?.substring(0, 100)}
+                  </div>
                 </div>
               );
             },

+ 21 - 9
dashboard-v4/dashboard/src/components/task/Description.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from "react";
-import { Button, message } from "antd";
+import { Button, message, Space } from "antd";
 import { EditOutlined, CheckOutlined } from "@ant-design/icons";
 
 import { ITaskData, ITaskResponse, ITaskUpdateRequest } from "../api/task";
@@ -7,6 +7,7 @@ import MdView from "../template/MdView";
 import MDEditor from "@uiw/react-md-editor";
 import "../article/article.css";
 import { patch } from "../../request";
+import { openDiscussion } from "../discussion/DiscussionButton";
 
 interface IWidget {
   task?: ITaskData;
@@ -30,14 +31,25 @@ const Description = ({ task, onChange }: IWidget) => {
         <span></span>
         <span>
           {mode === "read" ? (
-            <Button
-              ghost
-              type="primary"
-              icon={<EditOutlined />}
-              onClick={() => setMode("edit")}
-            >
-              编辑
-            </Button>
+            <Space>
+              <Button
+                key={1}
+                onClick={() => {
+                  task && openDiscussion(task?.id, "task", false);
+                }}
+              >
+                讨论
+              </Button>
+              <Button
+                key={2}
+                ghost
+                type="primary"
+                icon={<EditOutlined />}
+                onClick={() => setMode("edit")}
+              >
+                编辑
+              </Button>
+            </Space>
           ) : (
             <Button
               ghost

+ 6 - 24
dashboard-v4/dashboard/src/components/task/Task.tsx

@@ -1,7 +1,5 @@
-import { useState } from "react";
 import { ITaskData } from "../api/task";
 import TaskReader from "./TaskReader";
-import TaskEdit from "./TaskEdit";
 
 interface IWidget {
   taskId?: string;
@@ -9,30 +7,14 @@ interface IWidget {
   onChange?: (task: ITaskData[]) => void;
 }
 const Task = ({ taskId, onLoad, onChange }: IWidget) => {
-  const [isEdit, setIsEdit] = useState(false);
-  const [task, setTask] = useState<ITaskData>();
   return (
     <div>
-      {isEdit ? (
-        <TaskEdit
-          taskId={taskId}
-          onLoad={(data: ITaskData) => {}}
-          onChange={(data: ITaskData) => {
-            onChange && onChange([data]);
-            setTask(data);
-            setIsEdit(false);
-          }}
-        />
-      ) : (
-        <TaskReader
-          taskId={taskId}
-          onChange={(data: ITaskData[]) => {
-            onChange && onChange(data);
-            setTask(data.find((t) => t.id === taskId));
-          }}
-          onEdit={() => setIsEdit(true)}
-        />
-      )}
+      <TaskReader
+        taskId={taskId}
+        onChange={(data: ITaskData[]) => {
+          onChange && onChange(data);
+        }}
+      />
     </div>
   );
 };

+ 1 - 11
dashboard-v4/dashboard/src/components/task/TaskEditButton.tsx

@@ -4,7 +4,6 @@ import {
   CodeSandboxOutlined,
   DeleteOutlined,
   FieldTimeOutlined,
-  EditOutlined,
   ArrowRightOutlined,
 } from "@ant-design/icons";
 import { useIntl } from "react-intl";
@@ -19,10 +18,9 @@ interface IWidget {
   task?: ITaskData;
   studioName?: string;
   onChange?: (task: ITaskData[]) => void;
-  onEdit?: () => void;
   onPreTask?: (type: TRelation) => void;
 }
-const TaskEditButton = ({ task, onChange, onEdit, onPreTask }: IWidget) => {
+const TaskEditButton = ({ task, onChange, onPreTask }: IWidget) => {
   const intl = useIntl();
 
   const setValue = (setting: ITaskUpdateRequest) => {
@@ -39,11 +37,6 @@ const TaskEditButton = ({ task, onChange, onEdit, onPreTask }: IWidget) => {
   };
 
   const mainMenuItems: MenuProps["items"] = [
-    {
-      key: "edit",
-      label: intl.formatMessage({ id: "buttons.edit" }),
-      icon: <EditOutlined />,
-    },
     {
       key: "milestone",
       label: task?.is_milestone ? "取消里程碑" : "设为里程碑",
@@ -76,9 +69,6 @@ const TaskEditButton = ({ task, onChange, onEdit, onPreTask }: IWidget) => {
   ];
   const mainMenuClick: MenuProps["onClick"] = (e) => {
     switch (e.key) {
-      case "edit":
-        onEdit && onEdit();
-        break;
       case "milestone":
         if (task) {
           if (task.id) {

+ 19 - 12
dashboard-v4/dashboard/src/components/task/TaskReader.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useState } from "react";
 
-import { Divider, Space, Tag, Typography, message } from "antd";
+import { Divider, Skeleton, Space, Tag, Typography, message } from "antd";
 import { CodeSandboxOutlined } from "@ant-design/icons";
 
 import { ITaskData, ITaskResponse, ITaskUpdateRequest } from "../api/task";
@@ -16,13 +16,16 @@ import TaskTitle from "./TaskTitle";
 import TaskStatus from "./TaskStatus";
 import Description from "./Description";
 import Category from "./Category";
+import { useIntl } from "react-intl";
 
 const { Text } = Typography;
 
 export const Milestone = ({ task }: { task?: ITaskData }) => {
+  const intl = useIntl();
+
   return task?.is_milestone ? (
     <Tag icon={<CodeSandboxOutlined />} color="error">
-      里程碑
+      {intl.formatMessage({ id: "labels.milestone" })}
     </Tag>
   ) : null;
 };
@@ -30,21 +33,24 @@ export const Milestone = ({ task }: { task?: ITaskData }) => {
 interface IWidget {
   taskId?: string;
   onChange?: (data: ITaskData[]) => void;
-  onEdit?: () => void;
 }
-const TaskReader = ({ taskId, onChange, onEdit }: IWidget) => {
+const TaskReader = ({ taskId, onChange }: IWidget) => {
   const [openPreTask, setOpenPreTask] = useState(false);
   const [openNextTask, setOpenNextTask] = useState(false);
   const [task, setTask] = useState<ITaskData>();
+  const [loading, setLoading] = useState(true);
   useEffect(() => {
     const url = `/v2/task/${taskId}`;
     console.info("task api request", url);
-    get<ITaskResponse>(url).then((json) => {
-      console.info("task api response", json);
-      if (json.ok) {
-        setTask(json.data);
-      }
-    });
+    setLoading(true);
+    get<ITaskResponse>(url)
+      .then((json) => {
+        console.info("task api response", json);
+        if (json.ok) {
+          setTask(json.data);
+        }
+      })
+      .finally(() => setLoading(false));
   }, [taskId]);
 
   const updatePreTask = (type: TRelation, data?: ITaskData | null) => {
@@ -96,7 +102,9 @@ const TaskReader = ({ taskId, onChange, onEdit }: IWidget) => {
       }
     });
   };
-  return (
+  return loading ? (
+    <Skeleton active />
+  ) : (
     <div>
       <div style={{ display: "flex", justifyContent: "space-between" }}>
         <Space>
@@ -132,7 +140,6 @@ const TaskReader = ({ taskId, onChange, onEdit }: IWidget) => {
               setTask(tasks.find((value) => value.id === taskId));
               onChange && onChange(tasks);
             }}
-            onEdit={onEdit}
             onPreTask={(type: TRelation) => {
               if (type === "pre") {
                 setOpenPreTask(true);

+ 1 - 0
dashboard-v4/dashboard/src/locales/en-US/label.ts

@@ -85,6 +85,7 @@ const items = {
   "labels.filters.and": "and",
   "labels.filters.or": "or",
   "labels.task.workflows": "Workflows",
+  "labels.milestone": "milestone",
 };
 
 export default items;

+ 1 - 0
dashboard-v4/dashboard/src/locales/zh-Hans/label.ts

@@ -93,6 +93,7 @@ const items = {
   "labels.filters.and": "全部条件",
   "labels.filters.or": "任一条件",
   "labels.task.workflows": "工作流",
+  "labels.milestone": "里程碑",
 };
 
 export default items;