Forráskód Böngészése

Merge pull request #2283 from visuddhinanda/development

Development
visuddhinanda 11 hónapja
szülő
commit
baa25a8213

+ 29 - 0
api-v8/app/Console/Commands/MqAiTranslate.php

@@ -184,6 +184,35 @@ class MqAiTranslate extends Command
                         Log::info("{$queue} sentence update {$count} successful");
                     }
                 }
+                if ($message->task->info->category === 'suggest') {
+                    //写入pr
+                    $url = config('app.url') . '/api/v2/sentpr';
+                    Log::info($queue . " sentence update {$url}");
+                    $response = Http::timeout(10)->withToken($token)->post($url, [
+                        'book' => $message->sentence->book_id,
+                        'para' => $message->sentence->paragraph,
+                        'begin' => $message->sentence->word_start,
+                        'end' => $message->sentence->word_end,
+                        'channel' => $message->sentence->channel_uid,
+                        'text' => $responseContent
+                    ]);
+                    if ($response->failed()) {
+                        Log::error($queue . ' sentence update failed', [
+                            'url' => $url,
+                            'data' => $response->json(),
+                        ]);
+                        continue;
+                    } else {
+                        if ($response->json()['ok']) {
+                            Log::info("{$queue} sentence suggest update successful");
+                        } else {
+                            Log::error("{$queue} sentence suggest update failed", [
+                                'url' => $url,
+                                'data' => $response->json(),
+                            ]);
+                        }
+                    }
+                }
 
                 //写入discussion
                 #获取句子id

+ 5 - 2
api-v8/app/Http/Controllers/AccessTokenController.php

@@ -44,13 +44,16 @@ class AccessTokenController extends Controller
             //鉴权
             switch ($value['res_type']) {
                 case 'channel':
+                    if (!isset($value['power']) || !isset($value['res_id'])) {
+                        continue 2;
+                    }
                     if ($value['power'] === 'edit') {
                         if (!ChannelApi::userCanEdit($user['user_uid'], $value['res_id'])) {
-                            continue;
+                            continue 2;
                         }
                     } else {
                         if (!ChannelApi::userCanRead($user['user_uid'], $value['res_id'])) {
-                            continue;
+                            continue 2;
                         }
                     }
                     break;

+ 34 - 7
api-v8/app/Http/Controllers/ChannelController.php

@@ -36,22 +36,30 @@ class ChannelController extends Controller
         //
         $result = false;
         $indexCol = [
-            'uid',
+            'channels.uid',
             'name',
-            'summary',
+            'channels.summary',
             'type',
             'owner_uid',
-            'lang',
+            'channels.lang',
             'status',
             'is_system',
-            'updated_at',
-            'created_at'
+            'channels.updated_at',
+            'channels.created_at'
         ];
+        if ($request->has("book")) {
+            $indexCol[] = 'progress_chapters.progress';
+        }
         switch ($request->get('view')) {
             case 'public':
                 $table = Channel::select($indexCol)
                     ->where('status', 30);
-
+                /*
+                if ($request->has("book")) {
+                    $table = $table->leftJoin('progress_chapters', 'channels.uid', '=', 'progress_chapters.channel_id',)
+                        ->where('progress_chapters.book', $request->get("book"))
+                        ->where('progress_chapters.para', $request->get("paragraph"));
+                }*/
                 break;
             case 'studio':
                 # 获取studio内所有channel
@@ -75,7 +83,7 @@ class ChannelController extends Controller
                     foreach ($resList as $res) {
                         $resId[] = $res['res_id'];
                     }
-                    $table = $table->whereIn('uid', $resId);
+                    $table = $table->whereIn('channels.uid', $resId);
                     if ($request->get('collaborator', 'all') !== 'all') {
                         $table = $table->where('owner_uid', $request->get('collaborator'));
                     } else {
@@ -179,6 +187,24 @@ class ChannelController extends Controller
                 $table = Channel::select($indexCol)
                     ->whereIn('uid', explode(',', $request->get("id")));
         }
+
+        if ($request->has("book")) {
+            if ($request->get("view") === "public") {
+                $table = $table->leftJoin('progress_chapters', 'channels.uid', '=', 'progress_chapters.channel_id',)
+                    ->where('progress_chapters.book', $request->get("book"))
+                    ->where('progress_chapters.para', $request->get("paragraph"));
+            } else {
+                $table = $table->leftJoin('progress_chapters', function ($join) use ($request) {
+                    $join->on('channels.uid', '=', 'progress_chapters.channel_id')
+                        ->where('progress_chapters.book', $request->get("book"))
+                        ->where('progress_chapters.para', $request->get("paragraph")); // 条件写在这里!
+                });
+            }
+
+            /* leftJoin('progress_chapters', 'channels.uid', '=', 'progress_chapters.channel_id',)
+                ->where('progress_chapters.book', $request->get("book"))
+                ->where('progress_chapters.para', $request->get("paragraph"));*/
+        }
         //处理搜索
         if (!empty($request->get("search"))) {
             $table = $table->where('name', 'like', "%" . $request->get("search") . "%");
@@ -202,6 +228,7 @@ class ChannelController extends Controller
         //处理分页
         $table = $table->skip($request->get("offset", 0))
             ->take($request->get("limit", 200));
+        Log::debug('channel sql ' . $table->toSql());
         //获取数据
         $result = $table->get();
         //TODO 将下面代码转移到resource

+ 4 - 1
api-v8/app/Http/Resources/ChannelResource.php

@@ -27,7 +27,10 @@ class ChannelResource extends JsonResource
             "created_at" => $this->created_at,
             "updated_at" => $this->updated_at,
         ];
-        if(isset($this->role)){
+        if (isset($this->progress)) {
+            $data["progress"] = $this->progress;
+        }
+        if (isset($this->role)) {
             $data["role"] = $this->role;
         }
         return $data;

+ 1 - 0
dashboard-v4/dashboard/src/components/api/Channel.ts

@@ -33,6 +33,7 @@ export interface IApiResponseChannelData {
   lang: string;
   status: number;
   is_system: boolean;
+  progress?: number;
   created_at: string;
   updated_at: string;
   role?: TRole;

+ 2 - 0
dashboard-v4/dashboard/src/components/api/task.ts

@@ -37,12 +37,14 @@ export interface IProject {
 
 export type TTaskCategory =
   | "translate"
+  | "suggest"
   | "vocabulary"
   | "team"
   | "review"
   | "proofread";
 export const ATaskCategory: TTaskCategory[] = [
   "translate",
+  "suggest",
   "vocabulary",
   "team",
   "review",

+ 94 - 76
dashboard-v4/dashboard/src/components/channel/ChannelSelectWithToken.tsx

@@ -1,92 +1,110 @@
-import { ProFormSelect } from "@ant-design/pro-components";
-import { Space } from "antd";
-import { IApiResponseChannelList } from "../api/Channel";
-import { get } from "../../request";
 import { useState } from "react";
+import { Button, Input, Space, Tooltip, Typography } from "antd";
+import {
+  FolderOpenOutlined,
+  CheckCircleTwoTone,
+  LoadingOutlined,
+  WarningTwoTone,
+} from "@ant-design/icons";
+
+import { TChannelType } from "../api/Channel";
+import { post } from "../../request";
+import ChannelTableModal from "./ChannelTableModal";
+import { IChannel } from "./Channel";
+import {
+  IPayload,
+  ITokenCreate,
+  ITokenCreateResponse,
+  TPower,
+} from "../api/token";
+
+const { Text } = Typography;
+
+interface IData {
+  value: string;
+  label: string;
+}
 
 interface IWidget {
   channelsId?: string[];
-  type?: string;
+  type?: TChannelType;
+  book?: number;
+  para?: number;
+  power?: TPower;
   onChange?: (channel?: string | null) => void;
 }
-const ChannelSelectWithToken = ({ channelsId, type, onChange }: IWidget) => {
-  const [channel, setChannel] = useState<string>("");
-  const [power, setPower] = useState<string>();
+const ChannelSelectWithToken = ({
+  channelsId,
+  type,
+  book,
+  para,
+  power,
+  onChange,
+}: IWidget) => {
+  const [curr, setCurr] = useState<IData>();
+  const [access, setAccess] = useState<boolean>();
+  const [loading, setLoading] = useState(false);
   return (
     <Space>
-      <ProFormSelect
-        options={[]}
-        initialValue="translation"
-        width="md"
-        name="channel"
-        allowClear={true}
-        label={false}
-        placeholder={"选择一个channel"}
-        fieldProps={{
-          onChange(value: string, option) {
-            console.debug(value);
-
-            setChannel(value);
-            let output = value;
-            if (value) {
-              if (power) {
-                output += "@" + power;
-              }
-            }
-            onChange && onChange(output);
-          },
-        }}
-        request={async ({ keyWords }) => {
-          if (!channelsId) {
-            return [];
+      <Input
+        allowClear
+        value={curr?.label}
+        placeholder="选择一个版本"
+        onChange={(event) => {
+          if (event.target.value.trim().length === 0) {
+            setCurr(undefined);
+            setAccess(undefined);
+            onChange && onChange(undefined);
           }
-
-          const url = `/v2/channel?view=id&id=` + channelsId?.join(",");
-          console.info("api request", url);
-          const json = await get<IApiResponseChannelList>(url);
-          console.info("api response", json, type);
-          const textbookList = json.data.rows.map((item) => {
-            return {
-              value: item.uid,
-              label: item.name,
-            };
-          });
-          const current = json.data.rows.filter((value) => {
-            if (type) {
-              return value.type === type;
-            } else {
-              return true;
-            }
-          });
-          console.log("json", textbookList);
-          return textbookList;
         }}
       />
-      <ProFormSelect
-        options={[
-          { value: "readonly", label: "readonly" },
-          { value: "edit", label: "edit" },
-        ]}
-        initialValue="null"
-        width="xs"
-        name="power"
-        allowClear={true}
-        label={false}
-        placeholder={"选择访问权限"}
-        fieldProps={{
-          onChange(value: string, option) {
-            console.debug(value);
-            setPower(value);
-            let output = channel;
-            if (channel) {
-              if (value) {
-                output += "@" + value;
-              }
-            }
-            onChange && onChange(output);
-          },
+      <ChannelTableModal
+        chapter={book && para ? { book: book, paragraph: para } : undefined}
+        channelType={type}
+        trigger={<Button icon={<FolderOpenOutlined />} type="text" />}
+        onSelect={(channel: IChannel) => {
+          setCurr({ value: channel.id, label: channel.name });
+          //验证权限
+          if (power) {
+            setLoading(true);
+            let payload: IPayload[] = [];
+            payload.push({
+              res_id: channel.id,
+              res_type: "channel",
+              power: power,
+            });
+            const url = "/v2/access-token";
+            const values = { payload: payload };
+            console.info("token api request", url, values);
+            post<ITokenCreate, ITokenCreateResponse>(url, values)
+              .then((json) => {
+                console.info("token api response", json);
+                if (json.ok) {
+                  if (json.data.count > 0) {
+                    setAccess(true);
+                  }
+                }
+              })
+              .finally(() => setLoading(false));
+          }
+
+          onChange && onChange(channel.id + (power ? "@" + power : ""));
         }}
       />
+      <Text type="secondary">{power}</Text>
+      {loading ? (
+        <LoadingOutlined />
+      ) : typeof access !== "undefined" ? (
+        access ? (
+          <CheckCircleTwoTone twoToneColor="#52c41a" />
+        ) : (
+          <Tooltip title="无法获取指定的权限">
+            <WarningTwoTone twoToneColor="#eb2f96" />
+          </Tooltip>
+        )
+      ) : (
+        <></>
+      )}
     </Space>
   );
 };

+ 55 - 41
dashboard-v4/dashboard/src/components/channel/ChannelTable.tsx

@@ -1,7 +1,7 @@
 import { ActionType, ProTable } from "@ant-design/pro-components";
 import { FormattedMessage, useIntl } from "react-intl";
 import { Link } from "react-router-dom";
-import { Alert, Badge, message, Modal, Typography } from "antd";
+import { Alert, Badge, message, Modal, Progress, Typography } from "antd";
 import { Button, Dropdown, Popover } from "antd";
 import {
   PlusOutlined,
@@ -77,6 +77,11 @@ export const renderBadge = (count: number, active = false) => {
   );
 };
 
+export interface IChapter {
+  book: number;
+  paragraph: number;
+}
+
 interface IChannelItem {
   id: number;
   uid: string;
@@ -86,6 +91,7 @@ interface IChannelItem {
   role?: TRole;
   studio?: IStudio;
   publicity: number;
+  progress?: number;
   created_at: string;
 }
 
@@ -94,6 +100,7 @@ interface IWidget {
   type?: string;
   disableChannels?: string[];
   channelType?: TChannelType;
+  chapter?: IChapter;
   onSelect?: Function;
 }
 
@@ -102,6 +109,7 @@ const ChannelTableWidget = ({
   disableChannels,
   channelType,
   type,
+  chapter,
   onSelect,
 }: IWidget) => {
   const intl = useIntl();
@@ -122,7 +130,7 @@ const ChannelTableWidget = ({
 
   useEffect(() => {
     /**
-     * 获取各种课程的数量
+     * 获取各种channel的数量
      */
     const url = `/v2/channel-my-number?studio=${studioName}`;
     console.log("url", url);
@@ -236,6 +244,22 @@ const ChannelTableWidget = ({
               );
             },
           },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.createdAt.label",
+            }),
+            key: "progress",
+            hideInTable: typeof chapter === "undefined",
+            render(dom, entity, index, action, schema) {
+              return (
+                <Progress
+                  size="small"
+                  percent={Math.floor((entity.progress ?? 0) * 100)}
+                  style={{ width: 150 }}
+                />
+              );
+            },
+          },
           {
             title: intl.formatMessage({
               id: "forms.fields.summary.label",
@@ -324,6 +348,7 @@ const ChannelTableWidget = ({
             key: "option",
             width: 100,
             valueType: "option",
+            hideInTable: activeKey !== "my",
             render: (text, row, index, action) => {
               return [
                 <Dropdown.Button
@@ -387,46 +412,17 @@ const ChannelTableWidget = ({
             },
           },
         ]}
-        /*
-        rowSelection={{
-          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-          // 注释该行则默认不显示下拉选项
-          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-        }}
-        tableAlertRender={({
-          selectedRowKeys,
-          selectedRows,
-          onCleanSelected,
-        }) => (
-          <Space size={24}>
-            <span>
-              {intl.formatMessage({ id: "buttons.selected" })}
-              {selectedRowKeys.length}
-              <Button
-                type="link"
-                style={{ marginInlineStart: 8 }}
-                onClick={onCleanSelected}
-              >
-                {intl.formatMessage({ id: "buttons.unselect" })}
-              </Button>
-            </span>
-          </Space>
-        )}
-        tableAlertOptionRender={() => {
-          return (
-            <Space size={16}>
-              <Button type="link">
-                {intl.formatMessage({
-                  id: "buttons.delete.all",
-                })}
-              </Button>
-            </Space>
-          );
-        }}
-        */
         request={async (params = {}, sorter, filter) => {
           console.log(params, sorter, filter);
-          let url = `/v2/channel?view=studio&view2=${activeKey}&name=${studioName}`;
+          let url = `/v2/channel?`;
+          if (activeKey === "community") {
+            url += `view=public`;
+          } else {
+            url += `view=studio&view2=${activeKey}&name=${studioName}`;
+          }
+          if (chapter) {
+            url += `&book=${chapter.book}&paragraph=${chapter.paragraph}`;
+          }
           const offset =
             ((params.current ? params.current : 1) - 1) *
             (params.pageSize ? params.pageSize : 20);
@@ -435,7 +431,12 @@ const ChannelTableWidget = ({
           url += collaborator ? "&collaborator=" + collaborator : "";
           url += params.keyword ? "&search=" + params.keyword : "";
           url += channelType ? "&type=" + channelType : "";
-          url += getSorterUrl(sorter);
+          if (chapter && activeKey === "community") {
+            url += "&order=progress";
+          } else {
+            url += getSorterUrl(sorter);
+          }
+
           console.log("url", url);
           const res: IApiResponseChannelList = await get(url);
           const items: IChannelItem[] = res.data.rows.map((item, id) => {
@@ -446,6 +447,7 @@ const ChannelTableWidget = ({
               summary: item.summary,
               type: item.type,
               role: item.role,
+              progress: item.progress,
               studio: item.studio,
               publicity: item.status,
               created_at: item.created_at,
@@ -524,6 +526,18 @@ const ChannelTableWidget = ({
                   </span>
                 ),
               },
+              {
+                key: "community",
+                label: (
+                  <span>
+                    {intl.formatMessage({ id: "labels.community" })}
+                    {renderBadge(
+                      collaborationNumber,
+                      activeKey === "community"
+                    )}
+                  </span>
+                ),
+              },
             ],
             onChange(key) {
               console.log("show course", key);

+ 5 - 1
dashboard-v4/dashboard/src/components/channel/ChannelTableModal.tsx

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
 import { Modal } from "antd";
 
 import { ArticleType } from "../article/Article";
-import ChannelTable from "./ChannelTable";
+import ChannelTable, { IChapter } from "./ChannelTable";
 import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
 import { IChannel } from "./Channel";
@@ -17,6 +17,7 @@ interface IWidget {
   multiSelect?: boolean;
   disableChannels?: string[];
   open?: boolean;
+  chapter?: IChapter;
   onClose?: Function;
   onSelect?: Function;
 }
@@ -28,6 +29,7 @@ const ChannelTableModalWidget = ({
   disableChannels,
   channelType,
   open = false,
+  chapter,
   onClose,
   onSelect,
 }: IWidget) => {
@@ -64,6 +66,7 @@ const ChannelTableModalWidget = ({
         title={intl.formatMessage({
           id: "buttons.select.channel",
         })}
+        destroyOnClose
         footer={false}
         open={isModalOpen}
         onOk={handleOk}
@@ -73,6 +76,7 @@ const ChannelTableModalWidget = ({
           <ChannelTable
             studioName={user?.realName}
             type={type}
+            chapter={chapter}
             channelType={channelType}
             disableChannels={disableChannels}
             onSelect={(channel: IChannel) => {

+ 39 - 28
dashboard-v4/dashboard/src/components/task/TaskBuilderChapter.tsx

@@ -128,34 +128,6 @@ const TaskBuilderChapter = ({
                   setTitle(data[0].text);
                 }
               }
-              //获取channel token
-              let payload: IPayload[] = [];
-              channels?.forEach((channel) => {
-                data.forEach((chapter) => {
-                  const power: TPower[] = ["readonly", "edit"];
-                  payload = payload.concat(
-                    power.map((item) => {
-                      return {
-                        res_id: channel,
-                        res_type: "channel",
-                        book: chapter.book,
-                        para_start: chapter.paragraph,
-                        para_end: chapter.paragraph + chapter.chapter_len,
-                        power: item,
-                      };
-                    })
-                  );
-                });
-              });
-              const url = "/v2/access-token";
-              const values = { payload: payload };
-              console.info("api request", url, values);
-              post<ITokenCreate, ITokenCreateResponse>(url, values).then(
-                (json) => {
-                  console.info("api response", json);
-                  setTokens(json.data.rows);
-                }
-              );
             }}
           />
         </div>
@@ -183,11 +155,50 @@ const TaskBuilderChapter = ({
       content: (
         <div>
           <TaskBuilderProp
+            book={book}
+            para={para}
             workflow={workflow}
             channelsId={channels}
             onChange={(data: IProp[] | undefined) => {
               console.info("prop value", data);
               setProp(data);
+              let channels = new Map<string, number>();
+              data?.forEach((value) => {
+                value.param?.forEach((param) => {
+                  if (param.type.includes("channel")) {
+                    channels.set(param.value, 1);
+                  }
+                });
+              });
+              //获取channel token
+              let payload: IPayload[] = [];
+              if (chapter) {
+                channels.forEach((value, key) => {
+                  const [channelId, power] = key.split("@");
+                  payload = payload.concat(
+                    chapter.map((item) => {
+                      return {
+                        res_id: channelId,
+                        res_type: "channel",
+                        book: item.book,
+                        para_start: item.paragraph,
+                        para_end: item.paragraph + item.chapter_len,
+                        power: power as TPower,
+                      };
+                    })
+                  );
+                });
+
+                const url = "/v2/access-token";
+                const values = { payload: payload };
+                console.info("api request", url, values);
+                post<ITokenCreate, ITokenCreateResponse>(url, values).then(
+                  (json) => {
+                    console.info("api response", json);
+                    setTokens(json.data.rows);
+                  }
+                );
+              }
             }}
           />
         </div>

+ 16 - 3
dashboard-v4/dashboard/src/components/task/TaskBuilderProp.tsx

@@ -4,6 +4,8 @@ import { ITaskData } from "../api/task";
 import "../article/article.css";
 import { useEffect, useState } from "react";
 import ChannelSelectWithToken from "../channel/ChannelSelectWithToken";
+import { TChannelType } from "../api/Channel";
+import { TPower } from "../api/token";
 
 type TParamType =
   | "number"
@@ -27,9 +29,17 @@ export interface IProp {
 interface IWidget {
   workflow?: ITaskData[];
   channelsId?: string[];
+  book?: number;
+  para?: number;
   onChange?: (data: IProp[] | undefined) => void;
 }
-const TaskBuilderProp = ({ workflow, channelsId, onChange }: IWidget) => {
+const TaskBuilderProp = ({
+  workflow,
+  channelsId,
+  book,
+  para,
+  onChange,
+}: IWidget) => {
   //console.debug("TaskBuilderProp render");
   const [prop, setProp] = useState<IProp[]>();
   useEffect(() => {
@@ -113,8 +123,8 @@ const TaskBuilderProp = ({ workflow, channelsId, onChange }: IWidget) => {
 
   const Value = (item: IParam, taskId: number, paramId: number) => {
     let channelType: string | undefined;
+    const [key, channel, power] = item.key.replaceAll("%", "").split("@");
     if (item.key.includes("@channel")) {
-      const [_, channel] = item.key.split("@");
       if (channel.includes(":")) {
         channelType = channel.split(":")[1].replaceAll("%", "");
       }
@@ -142,7 +152,10 @@ const TaskBuilderProp = ({ workflow, channelsId, onChange }: IWidget) => {
     ) : (
       <ChannelSelectWithToken
         channelsId={channelsId}
-        type={channelType}
+        book={book}
+        para={para}
+        type={channelType as TChannelType}
+        power={power ? (power as TPower) : undefined}
         onChange={(e) => {
           console.debug("channel select onChange", e);
           change(taskId, paramId, e ?? "", item.initValue, item.step);

+ 1 - 7
dashboard-v4/dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -1,12 +1,6 @@
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
-import {
-  Divider,
-  message as AntdMessage,
-  Modal,
-  Collapse,
-  CollapseProps,
-} from "antd";
+import { Divider, message as AntdMessage, Modal, Collapse } from "antd";
 import { ExclamationCircleOutlined, LoadingOutlined } from "@ant-design/icons";
 
 import { ISentence } from "../SentEdit";

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

@@ -75,6 +75,7 @@ const items = {
   "labels.task.category.team": "team",
   "labels.task.category.review": "review",
   "labels.task.category.proofread": "proofread",
+  "labels.task.category.suggest": "suggest",
   "labels.ai-assistant": "AI Assistant",
   "labels.filters.includes": "includes",
   "labels.filters.not-includes": "not includes",

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

@@ -82,7 +82,8 @@ const items = {
   "labels.task.category.vocabulary": "词汇表",
   "labels.task.category.team": "术语",
   "labels.task.category.review": "审稿",
-  "labels.task.category.proofread": "proofread",
+  "labels.task.category.proofread": "润色",
+  "labels.task.category.suggest": "修改建议",
   "labels.ai-assistant": "人工智能助手",
   "labels.filters.includes": "包含",
   "labels.filters.not-includes": "不包含",