Просмотр исходного кода

Merge pull request #1342 from visuddhinanda/agile

discussion 用 prolist
visuddhinanda 2 лет назад
Родитель
Сommit
98e043088c
100 измененных файлов с 8621 добавлено и 1135 удалено
  1. 13 2
      dashboard/src/Router.tsx
  2. 10 14
      dashboard/src/components/anthology/AnthologyList.tsx
  3. 8 1
      dashboard/src/components/api/Comment.ts
  4. 1 1
      dashboard/src/components/api/Corpus.ts
  5. 42 0
      dashboard/src/components/api/webhook.ts
  6. 102 14
      dashboard/src/components/article/AnchorNav.tsx
  7. 1 2
      dashboard/src/components/article/AnthologyDetail.tsx
  8. 0 2
      dashboard/src/components/article/ArticleEdit.tsx
  9. 2 2
      dashboard/src/components/article/ArticleEditTools.tsx
  10. 14 10
      dashboard/src/components/article/ArticleList.tsx
  11. 5 4
      dashboard/src/components/article/ArticleListPublic.tsx
  12. 0 115
      dashboard/src/components/article/ArticleTplMaker.tsx
  13. 1 0
      dashboard/src/components/article/EditableTreeNode.tsx
  14. 3 3
      dashboard/src/components/article/RightPanel.tsx
  15. 5 1
      dashboard/src/components/article/ToolButtonNavSliceTitle.tsx
  16. 32 0
      dashboard/src/components/article/article.css
  17. 0 1
      dashboard/src/components/auth/StudioName.tsx
  18. 1 1
      dashboard/src/components/blog/BlogNav.tsx
  19. 7 1
      dashboard/src/components/blog/TimeLine.tsx
  20. 291 279
      dashboard/src/components/channel/ChannelPickerTable.tsx
  21. 26 13
      dashboard/src/components/channel/ChannelTable.tsx
  22. 4 0
      dashboard/src/components/channel/ChannelTableModal.tsx
  23. 9 11
      dashboard/src/components/channel/ChapterInChannelList.tsx
  24. 16 6
      dashboard/src/components/channel/CopyToModal.tsx
  25. 1 0
      dashboard/src/components/channel/CopyToStep.tsx
  26. 84 0
      dashboard/src/components/channel/Edit.tsx
  27. 0 1
      dashboard/src/components/corpus/BookTree.tsx
  28. 1 6
      dashboard/src/components/corpus/ChapterCard.tsx
  29. 1 1
      dashboard/src/components/corpus/ChapterChannelSelect.tsx
  30. 1 1
      dashboard/src/components/corpus/ChapterInChannel.tsx
  31. 11 8
      dashboard/src/components/corpus/PaliChapterListByTag.tsx
  32. 1 7
      dashboard/src/components/corpus/SentHistory.tsx
  33. 4 7
      dashboard/src/components/corpus/TopChapter.tsx
  34. 0 1
      dashboard/src/components/course/AddLesson.tsx
  35. 0 1
      dashboard/src/components/course/AddMember.tsx
  36. 0 1
      dashboard/src/components/course/AddStudent.tsx
  37. 0 1
      dashboard/src/components/course/CourseMember.tsx
  38. 1 1
      dashboard/src/components/dict/DictCreate.tsx
  39. 0 1
      dashboard/src/components/dict/DictEdit.tsx
  40. 8 0
      dashboard/src/components/dict/SelectCase.tsx
  41. 8 2
      dashboard/src/components/dict/UserDictList.tsx
  42. 24 14
      dashboard/src/components/discussion/DiscussionAnchor.tsx
  43. 10 5
      dashboard/src/components/discussion/DiscussionBox.tsx
  44. 1 0
      dashboard/src/components/discussion/DiscussionEdit.tsx
  45. 30 7
      dashboard/src/components/discussion/DiscussionItem.tsx
  46. 19 1
      dashboard/src/components/discussion/DiscussionList.tsx
  47. 175 92
      dashboard/src/components/discussion/DiscussionListCard.tsx
  48. 81 20
      dashboard/src/components/discussion/DiscussionShow.tsx
  49. 3 1
      dashboard/src/components/discussion/DiscussionTopic.tsx
  50. 27 20
      dashboard/src/components/discussion/DiscussionTopicChildren.tsx
  51. 13 11
      dashboard/src/components/discussion/DiscussionTopicInfo.tsx
  52. 5942 0
      dashboard/src/components/general/PaliEnding.ts
  53. 15 0
      dashboard/src/components/general/ParserError.tsx
  54. 108 31
      dashboard/src/components/general/TermTextAreaMenu.tsx
  55. 50 14
      dashboard/src/components/general/TimeShow.tsx
  56. 0 1
      dashboard/src/components/group/AddMember.tsx
  57. 0 1
      dashboard/src/components/group/GroupCreate.tsx
  58. 0 1
      dashboard/src/components/group/GroupFile.tsx
  59. 0 1
      dashboard/src/components/group/GroupMember.tsx
  60. 0 1
      dashboard/src/components/invite/InviteCreate.tsx
  61. 1 1
      dashboard/src/components/library/FooterBar.tsx
  62. 0 2
      dashboard/src/components/library/HeadBar.tsx
  63. 0 1
      dashboard/src/components/share/Collaborator.tsx
  64. 0 2
      dashboard/src/components/share/CollaboratorAdd.tsx
  65. 1 1
      dashboard/src/components/template/Article.tsx
  66. 246 0
      dashboard/src/components/template/Builder/ArticleTpl.tsx
  67. 74 0
      dashboard/src/components/template/Builder/Builder.tsx
  68. 7 2
      dashboard/src/components/template/MdView.tsx
  69. 0 1
      dashboard/src/components/template/Nissaya.tsx
  70. 1 1
      dashboard/src/components/template/ParaHandle.tsx
  71. 39 2
      dashboard/src/components/template/SentEdit.tsx
  72. 17 4
      dashboard/src/components/template/SentEdit/EditInfo.tsx
  73. 8 1
      dashboard/src/components/template/SentEdit/SentAdd.tsx
  74. 2 1
      dashboard/src/components/template/SentEdit/SentCanRead.tsx
  75. 86 75
      dashboard/src/components/template/SentEdit/SentCell.tsx
  76. 9 2
      dashboard/src/components/template/SentEdit/SentCellEditable.tsx
  77. 21 7
      dashboard/src/components/template/SentEdit/SentContent.tsx
  78. 13 5
      dashboard/src/components/template/SentEdit/SentEditMenu.tsx
  79. 45 7
      dashboard/src/components/template/SentEdit/SentMenu.tsx
  80. 211 163
      dashboard/src/components/template/SentEdit/SentTab.tsx
  81. 6 1
      dashboard/src/components/template/SentEdit/SentTabButton.tsx
  82. 1 1
      dashboard/src/components/template/SentEdit/SentWbwEdit.tsx
  83. 15 13
      dashboard/src/components/template/SentEdit/SuggestionAdd.tsx
  84. 103 43
      dashboard/src/components/template/SentEdit/SuggestionBox.tsx
  85. 9 5
      dashboard/src/components/template/SentEdit/SuggestionList.tsx
  86. 3 1
      dashboard/src/components/template/SentEdit/SuggestionToolbar.tsx
  87. 2 4
      dashboard/src/components/template/Term.tsx
  88. 25 17
      dashboard/src/components/template/Wbw/WbwPali.tsx
  89. 1 1
      dashboard/src/components/template/Wbw/WbwReal.tsx
  90. 9 0
      dashboard/src/components/template/Wbw/wbw.css
  91. 18 6
      dashboard/src/components/template/WbwSent.tsx
  92. 0 2
      dashboard/src/components/template/Wd.tsx
  93. 17 4
      dashboard/src/components/template/utilities.ts
  94. 1 2
      dashboard/src/components/term/TermItem.tsx
  95. 9 12
      dashboard/src/components/term/TermList.tsx
  96. 160 0
      dashboard/src/components/webhook/WebhookEdit.tsx
  97. 236 0
      dashboard/src/components/webhook/WebhookList.tsx
  98. 6 0
      dashboard/src/locales/zh-Hans/buttons.ts
  99. 2 2
      dashboard/src/locales/zh-Hans/dict/index.ts
  100. 5 0
      dashboard/src/locales/zh-Hans/forms.ts

+ 13 - 2
dashboard/src/Router.tsx

@@ -84,7 +84,7 @@ import StudioRecentList from "./pages/studio/recent/list";
 
 import StudioChannel from "./pages/studio/channel";
 import StudioChannelList from "./pages/studio/channel/list";
-import StudioChannelEdit from "./pages/studio/channel/edit";
+import StudioChannelSetting from "./pages/studio/channel/setting";
 import StudioChannelShow from "./pages/studio/channel/show";
 
 import StudioGroup from "./pages/studio/group";
@@ -262,7 +262,18 @@ const Widget = () => {
 
           <Route path="channel" element={<StudioChannel />}>
             <Route path="list" element={<StudioChannelList />} />
-            <Route path=":channelid/edit" element={<StudioChannelEdit />} />
+            <Route
+              path=":channelId/setting"
+              element={<StudioChannelSetting />}
+            />
+            <Route
+              path=":channelId/setting/:type"
+              element={<StudioChannelSetting />}
+            />
+            <Route
+              path=":channelId/setting/:type/:id"
+              element={<StudioChannelSetting />}
+            />
             <Route path=":channelId" element={<StudioChannelShow />} />
           </Route>
 

+ 10 - 14
dashboard/src/components/anthology/AnthologyList.tsx

@@ -23,7 +23,7 @@ import Share, { EResType } from "../share/Share";
 
 import StudioName, { IStudio } from "../auth/StudioName";
 import { IResNumberResponse, renderBadge } from "../channel/ChannelTable";
-import { fullUrl } from "../../utils";
+import { fullUrl, getSorterUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -35,7 +35,7 @@ interface IItem {
   publicity: number;
   articles: number;
   studio?: IStudio;
-  createdAt: number;
+  updated_at: string;
 }
 interface IWidget {
   title?: string;
@@ -204,14 +204,14 @@ const AnthologyListWidget = ({
           },
           {
             title: intl.formatMessage({
-              id: "forms.fields.created-at.label",
+              id: "forms.fields.updated-at.label",
             }),
-            key: "created-at",
+            key: "updated_at",
             width: 100,
             search: false,
-            dataIndex: "createdAt",
+            dataIndex: "updated_at",
             valueType: "date",
-            sorter: (a, b) => a.createdAt - b.createdAt,
+            sorter: true,
           },
           {
             title: intl.formatMessage({ id: "buttons.option" }),
@@ -279,7 +279,6 @@ const AnthologyListWidget = ({
           },
         ]}
         request={async (params = {}, sorter, filter) => {
-          // TODO
           console.log(params, sorter, filter);
           let url = `/v2/anthology?view=studio&view2=${activeKey}&name=${studioName}`;
           const offset =
@@ -287,23 +286,20 @@ const AnthologyListWidget = ({
             (params.pageSize ? params.pageSize : 20);
           url += `&limit=${params.pageSize}&offset=${offset}`;
           url += params.keyword ? "&search=" + params.keyword : "";
-          url += sorter.createdAt
-            ? "&order=created_at&dir=" +
-              (sorter.createdAt === "ascend" ? "asc" : "desc")
-            : "";
+
+          url += getSorterUrl(sorter);
 
           const res = await get<IAnthologyListResponse>(url);
           const items: IItem[] = res.data.rows.map((item, id) => {
-            const date = new Date(item.created_at);
             return {
-              sn: id + 1,
+              sn: id + offset + 1,
               id: item.uid,
               title: item.title,
               subtitle: item.subtitle,
               publicity: item.status,
               articles: item.childrenNumber,
               studio: item.studio,
-              createdAt: date.getTime(),
+              updated_at: item.updated_at,
             };
           });
           console.log(items);

+ 8 - 1
dashboard/src/components/api/Comment.ts

@@ -10,6 +10,7 @@ export interface ICommentRequest {
   content?: string;
   content_type?: TContentType;
   parent?: string;
+  status?: "active" | "close";
   editor?: IUserApiData;
   created_at?: string;
   updated_at?: string;
@@ -23,6 +24,7 @@ export interface ICommentApiData {
   content?: string;
   content_type?: TContentType;
   parent?: string;
+  status?: "active" | "close";
   children_count: number;
   editor: IUserApiData;
   created_at?: string;
@@ -38,7 +40,12 @@ export interface ICommentResponse {
 export interface ICommentListResponse {
   ok: boolean;
   message: string;
-  data: { rows: ICommentApiData[]; count: number };
+  data: {
+    rows: ICommentApiData[];
+    count: number;
+    active: number;
+    close: number;
+  };
 }
 export interface ICommentAnchorResponse {
   ok: boolean;

+ 1 - 1
dashboard/src/components/api/Corpus.ts

@@ -146,7 +146,7 @@ export interface ISentenceRequest {
   wordStart: number;
   wordEnd: number;
   channel: string;
-  content: string;
+  content: string | null;
   contentType?: TContentType;
   prEditor?: string;
   prId?: string;

+ 42 - 0
dashboard/src/components/api/webhook.ts

@@ -0,0 +1,42 @@
+import { IUser } from "../auth/User";
+import { TResType } from "../discussion/DiscussionListCard";
+
+export type TReceiverType = "wechat" | "dingtalk";
+
+export interface IWebhookRequest {
+  res_type: TResType;
+  res_id: string;
+  url: string;
+  receiver: TReceiverType;
+  event?: string[] | null;
+  status?: string;
+}
+
+export interface IWebhookApiData {
+  id: string;
+  res_type: TResType;
+  res_id: string;
+  url: string;
+  receiver: TReceiverType;
+  event: string[] | null;
+  fail: number;
+  success: number;
+  status: string;
+  editor: IUser;
+  created_at: string | null;
+  updated_at: string | null;
+}
+
+export interface IWebhookResponse {
+  ok: boolean;
+  message: string;
+  data: IWebhookApiData;
+}
+export interface IWebhookListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IWebhookApiData[];
+    count: number;
+  };
+}

+ 102 - 14
dashboard/src/components/article/AnchorNav.tsx

@@ -1,38 +1,126 @@
 import { Anchor } from "antd";
-import lodash from "lodash";
 import { useEffect, useState } from "react";
 const { Link } = Anchor;
 
 interface IHeadingAnchor {
   label: string;
   key: string;
+  level: number;
+  children?: IHeadingAnchor[];
 }
+
+function tocGetTreeData(
+  listData: IHeadingAnchor[],
+  active = ""
+): IHeadingAnchor[] | undefined {
+  let treeData: IHeadingAnchor[] = [];
+  let tocActivePath: IHeadingAnchor[] = [];
+  let treeParents = [];
+  let rootNode: IHeadingAnchor = {
+    key: "0",
+    label: "root",
+    level: 0,
+    children: [],
+  };
+  treeData.push(rootNode);
+  let lastInsNode: IHeadingAnchor = rootNode;
+
+  let iCurrLevel = 0;
+  for (let index = 0; index < listData.length; index++) {
+    const element = listData[index];
+
+    let newNode: IHeadingAnchor = {
+      key: element.key,
+      label: element.label,
+      level: element.level,
+    };
+
+    if (newNode.level > iCurrLevel) {
+      //新的层级比较大,为上一个的子目录
+      treeParents.push(lastInsNode);
+      if (typeof lastInsNode.children === "undefined") {
+        lastInsNode.children = [];
+      }
+      lastInsNode.children.push(newNode);
+    } else if (newNode.level === iCurrLevel) {
+      //目录层级相同,为平级
+      const parentNode = treeParents[treeParents.length - 1];
+      if (typeof parentNode !== "undefined") {
+        if (typeof parentNode.children === "undefined") {
+          parentNode.children = [];
+        }
+        parentNode.children.push(newNode);
+      }
+    } else {
+      // 小于 挂在上一个层级
+      while (treeParents.length > 1) {
+        treeParents.pop();
+        if (treeParents[treeParents.length - 1].level < newNode.level) {
+          break;
+        }
+      }
+      const parentNode = treeParents[treeParents.length - 1];
+      if (typeof parentNode !== "undefined") {
+        if (typeof parentNode.children === "undefined") {
+          parentNode.children = [];
+        }
+        parentNode.children.push(newNode);
+      }
+    }
+    lastInsNode = newNode;
+    iCurrLevel = newNode.level;
+
+    if (active === element.key) {
+      tocActivePath = [];
+      for (let index = 1; index < treeParents.length; index++) {
+        //treeParents[index]["expanded"] = true;
+        tocActivePath.push(treeParents[index]);
+      }
+    }
+  }
+
+  return treeData[0].children;
+}
+
 interface IWidget {
   content?: string;
   open?: boolean;
 }
 const AnchorNavWidget = ({ open = false, content }: IWidget) => {
-  const [heading, setHeading] = useState<IHeadingAnchor[]>([]);
+  const [heading, setHeading] = useState<IHeadingAnchor[]>();
 
   useEffect(() => {
     let heading = document.querySelectorAll("h1,h2,h3,h4,h5,h6");
     let headingAnchor: IHeadingAnchor[] = [];
     for (let index = 0; index < heading.length; index++) {
       const element = heading[index];
-      const id = lodash
-        .times(20, () => lodash.random(35).toString(36))
-        .join("");
-      heading[index].id = id;
-      headingAnchor.push({ key: `#${id}`, label: element.innerHTML });
+      const id = heading[index].id;
+      if (id) {
+        console.log("level", heading[index].tagName);
+        const level = parseInt(heading[index].tagName.replace("H", ""));
+        headingAnchor.push({
+          key: `#${id}`,
+          label: element.innerHTML,
+          level: level,
+        });
+      }
     }
-    setHeading(headingAnchor);
+    setHeading(tocGetTreeData(headingAnchor));
+    console.log("heading", headingAnchor);
   }, [open]);
-  return open ? (
-    <Anchor offsetTop={50}>
-      {heading.map((item, index) => {
-        return <Link key={index} href={item.key} title={item.label}></Link>;
-      })}
-    </Anchor>
+
+  const GetLink = (anchors: IHeadingAnchor[]) => {
+    return anchors.map((it, id) => {
+      return (
+        <Link key={id} href={it.key} title={it.label}>
+          {it.children ? GetLink(it.children) : undefined}
+        </Link>
+      );
+    });
+  };
+
+  return open && heading ? (
+    <Anchor offsetTop={50}>{GetLink(heading)}</Anchor>
   ) : (
     <></>
   );

+ 1 - 2
dashboard/src/components/article/AnthologyDetail.tsx

@@ -1,4 +1,3 @@
-import { useNavigate } from "react-router-dom";
 import { useState, useEffect } from "react";
 import { Space, Typography } from "antd";
 
@@ -68,7 +67,7 @@ const AnthologyDetailWidget = ({
       <Paragraph>
         <Space>
           <StudioName data={tableData?.studio} />
-          <TimeShow time={tableData?.updated_at} title="updated" />
+          <TimeShow updatedAt={tableData?.updated_at} />
         </Space>
       </Paragraph>
       <Paragraph>

+ 0 - 2
dashboard/src/components/article/ArticleEdit.tsx

@@ -72,8 +72,6 @@ const ArticleEditWidget = ({
       ) : undefined}
       <ProForm<IFormData>
         onFinish={async (values: IFormData) => {
-          // TODO
-
           const request = {
             uid: articleId ? articleId : "",
             title: values.title,

+ 2 - 2
dashboard/src/components/article/ArticleEditTools.tsx

@@ -3,7 +3,7 @@ import { useIntl } from "react-intl";
 import { TeamOutlined } from "@ant-design/icons";
 import { Button, Space } from "antd";
 
-import ArticleTplMaker from "../../components/article/ArticleTplMaker";
+import { ArticleTplModal } from "../template/Builder/ArticleTpl";
 import ShareModal from "../../components/share/ShareModal";
 import { EResType } from "../../components/share/Share";
 import AddToAnthology from "../../components/article/AddToAnthology";
@@ -40,7 +40,7 @@ const ArticleEditToolsWidget = ({
       <Link to={`/article/article/${articleId}`} target="_blank">
         {intl.formatMessage({ id: "buttons.open.in.library" })}
       </Link>
-      <ArticleTplMaker
+      <ArticleTplModal
         title={title}
         type="article"
         id={articleId}

+ 14 - 10
dashboard/src/components/article/ArticleList.tsx

@@ -29,12 +29,13 @@ import {
 } from "../../components/api/Article";
 import { PublicityValueEnum } from "../../components/studio/table";
 import { useEffect, useRef, useState } from "react";
-import ArticleTplMaker from "../../components/article/ArticleTplMaker";
+import { ArticleTplModal } from "../template/Builder/ArticleTpl";
 import Share, { EResType } from "../../components/share/Share";
 import AddToAnthology from "../../components/article/AddToAnthology";
 import AnthologySelect from "../../components/anthology/AnthologySelect";
 import StudioName, { IStudio } from "../../components/auth/StudioName";
 import { IUser } from "../../components/auth/User";
+import { getSorterUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -70,9 +71,9 @@ interface DataItem {
   anthologyCount?: number;
   anthologyTitle?: string;
   publicity: number;
-  createdAt: number;
   studio?: IStudio;
   editor?: IUser;
+  updated_at?: string;
 }
 
 interface IWidget {
@@ -251,14 +252,14 @@ const ArticleListWidget = ({
           },
           {
             title: intl.formatMessage({
-              id: "forms.fields.created-at.label",
+              id: "forms.fields.updated-at.label",
             }),
-            key: "created-at",
+            key: "updated_at",
             width: 100,
             search: false,
-            dataIndex: "createdAt",
+            dataIndex: "updated_at",
             valueType: "date",
-            sorter: (a, b) => a.createdAt - b.createdAt,
+            sorter: true,
           },
           {
             title: intl.formatMessage({ id: "buttons.option" }),
@@ -277,7 +278,7 @@ const ArticleListWidget = ({
                       {
                         key: "tpl",
                         label: (
-                          <ArticleTplMaker
+                          <ArticleTplModal
                             title={row.title}
                             type="article"
                             id={row.id}
@@ -386,17 +387,20 @@ const ArticleListWidget = ({
           const offset =
             ((params.current ? params.current : 1) - 1) *
             (params.pageSize ? params.pageSize : 10);
+
           url += `&limit=${params.pageSize}&offset=${offset}`;
           url += params.keyword ? "&search=" + params.keyword : "";
 
           if (typeof anthologyId !== "undefined") {
             url += "&anthology=" + anthologyId;
           }
+
+          url += getSorterUrl(sorter);
+
           const res = await get<IArticleListResponse>(url);
           const items: DataItem[] = res.data.rows.map((item, id) => {
-            const date = new Date(item.created_at);
             return {
-              sn: id + 1,
+              sn: id + offset + 1,
               id: item.uid,
               title: item.title,
               subtitle: item.subtitle,
@@ -404,7 +408,7 @@ const ArticleListWidget = ({
               anthologyCount: item.anthology_count,
               anthologyTitle: item.anthology_first?.title,
               publicity: item.status,
-              createdAt: date.getTime(),
+              updated_at: item.updated_at,
               studio: item.studio,
               editor: item.editor,
             };

+ 5 - 4
dashboard/src/components/article/ArticleListPublic.tsx

@@ -1,5 +1,4 @@
 import { Link } from "react-router-dom";
-import { useIntl } from "react-intl";
 import { useRef } from "react";
 import { Space } from "antd";
 import { ActionType, ProList } from "@ant-design/pro-components";
@@ -31,8 +30,6 @@ interface IWidget {
   studioName?: string;
 }
 const ArticleListWidget = ({ search, studioName }: IWidget) => {
-  const intl = useIntl(); //i18n
-
   const ref = useRef<ActionType>();
 
   return (
@@ -54,7 +51,11 @@ const ArticleListWidget = ({ search, studioName }: IWidget) => {
               return (
                 <Space>
                   {row.editor?.nickName}
-                  <TimeShow time={row.updatedAt} />
+                  <TimeShow
+                    updatedAt={row.updatedAt}
+                    showLabel={false}
+                    showIcon={false}
+                  />
                 </Space>
               );
             },

+ 0 - 115
dashboard/src/components/article/ArticleTplMaker.tsx

@@ -1,115 +0,0 @@
-import { useEffect, useState } from "react";
-import { Input, Modal, Select, Space, Typography } from "antd";
-
-import { TDisplayStyle } from "../template/Article";
-const { TextArea } = Input;
-const { Paragraph } = Typography;
-interface IWidget {
-  type?: string;
-  id?: string;
-  title?: string;
-  style?: TDisplayStyle;
-  trigger?: JSX.Element;
-  onSelect?: Function;
-  onCancel?: Function;
-}
-const ArticleTplMakerWidget = ({
-  type,
-  id,
-  title,
-  style = "modal",
-  trigger,
-  onSelect,
-  onCancel,
-}: IWidget) => {
-  const [isModalOpen, setIsModalOpen] = useState(false);
-  const [titleText, setTitleText] = useState(title);
-  const [styleText, setStyleText] = useState(style);
-  const [tplText, setTplText] = useState("");
-
-  const ids = id?.split("_");
-  const id1 = ids ? ids[0] : undefined;
-  const channels = ids
-    ? ids.length > 1
-      ? ids?.slice(1)
-      : undefined
-    : undefined;
-
-  const showModal = () => {
-    setIsModalOpen(true);
-  };
-
-  const handleOk = () => {
-    setIsModalOpen(false);
-  };
-
-  const handleCancel = () => {
-    setIsModalOpen(false);
-  };
-  useEffect(() => {
-    setTitleText(title);
-  }, [title]);
-  useEffect(() => {
-    let tplText = `{{article|
-type=${type}|
-id=${id1}|
-title=${titleText}|
-style=${styleText}`;
-    tplText += channels ? `channel=${channels}` : undefined;
-    tplText += "}}";
-
-    setTplText(tplText);
-  }, [titleText, styleText, type, id1, channels]);
-  return (
-    <>
-      <span onClick={showModal}>{trigger}</span>
-      <Modal
-        width={"80%"}
-        title="生成模版"
-        open={isModalOpen}
-        onOk={handleOk}
-        onCancel={handleCancel}
-      >
-        <Space direction="vertical" style={{ width: 500 }}>
-          <Space style={{ width: 500 }}>
-            {"标题:"}
-            <Input
-              width={400}
-              value={titleText}
-              placeholder="Basic usage"
-              onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
-                setTitleText(event.target.value);
-              }}
-            />
-          </Space>
-          <Space>
-            {"显示为:"}
-            <Select
-              defaultValue={style}
-              style={{ width: 120 }}
-              onChange={(value: string) => {
-                console.log(`selected ${value}`);
-                setStyleText(value as TDisplayStyle);
-              }}
-              options={[
-                { value: "modal", label: "对话框" },
-                { value: "card", label: "卡片" },
-              ]}
-            />
-          </Space>
-          <div>
-            <TextArea
-              value={tplText}
-              rows={4}
-              placeholder="maxLength is 6"
-              maxLength={6}
-            />
-            <Paragraph copyable={{ text: tplText }}>复制</Paragraph>
-          </div>
-        </Space>
-      </Modal>
-    </>
-  );
-};
-
-export default ArticleTplMakerWidget;

+ 1 - 0
dashboard/src/components/article/EditableTreeNode.tsx

@@ -1,6 +1,7 @@
 import { Button, message, Space, Typography } from "antd";
 import { useState } from "react";
 import { PlusOutlined, EditOutlined } from "@ant-design/icons";
+
 import { TreeNodeData } from "./EditableTree";
 const { Text } = Typography;
 

+ 3 - 3
dashboard/src/components/article/RightPanel.tsx

@@ -15,7 +15,7 @@ interface IWidget {
   curr?: TPanelName;
   type: ArticleType;
   articleId: string;
-  selectedChannelKeys?: string[];
+  selectedChannelsId?: string[];
   onChannelSelect?: Function;
   onClose?: Function;
   onTabChange?: Function;
@@ -25,7 +25,7 @@ const RightPanelWidget = ({
   type,
   articleId,
   onChannelSelect,
-  selectedChannelKeys,
+  selectedChannelsId,
   onClose,
   onTabChange,
 }: IWidget) => {
@@ -108,7 +108,7 @@ const RightPanelWidget = ({
                 <ChannelPickerTable
                   type={type}
                   articleId={articleId}
-                  selectedKeys={selectedChannelKeys}
+                  selectedKeys={selectedChannelsId}
                   onSelect={(e: IChannel[]) => {
                     console.log(e);
                     if (typeof onChannelSelect !== "undefined") {

+ 5 - 1
dashboard/src/components/article/ToolButtonNavSliceTitle.tsx

@@ -1,5 +1,6 @@
 import { Dropdown } from "antd";
 import React from "react";
+import { useIntl } from "react-intl";
 
 interface IWidget {
   label?: React.ReactNode;
@@ -7,6 +8,7 @@ interface IWidget {
 }
 
 const ToolButtonNavSliceTitleWidget = ({ label, onMenuClick }: IWidget) => {
+  const intl = useIntl();
   return (
     <Dropdown.Button
       type="text"
@@ -15,7 +17,9 @@ const ToolButtonNavSliceTitleWidget = ({ label, onMenuClick }: IWidget) => {
         items: [
           {
             key: "copy-link",
-            label: "复制链接",
+            label: intl.formatMessage({
+              id: "buttons.copy.link",
+            }),
           },
           {
             key: "open",

+ 32 - 0
dashboard/src/components/article/article.css

@@ -35,3 +35,35 @@ h6 {
   padding-left: 0.5em;
   color: gray;
 }
+
+.pcd_article table {
+  border-spacing: 0;
+  border-collapse: collapse;
+  display: block;
+  width: -webkit-max-content;
+  width: max-content;
+  max-width: 100%;
+}
+.pcd_article td,
+.pcd_article th {
+  padding: 0;
+}
+
+.pcd_article table th {
+  font-weight: 600;
+}
+.pcd_article table th,
+.pcd_article table td {
+  padding: 6px 13px;
+  border: 1px solid #d0d7de;
+}
+.pcd_article table tr {
+  background-color: #ffffff;
+  border-top: 1px solid hsl(210, 18%, 87%);
+}
+.pcd_article table tr:nth-child(2n) {
+  background-color: #f6f8fa;
+}
+.pcd_article table img {
+  background-color: transparent;
+}

+ 0 - 1
dashboard/src/components/auth/StudioName.tsx

@@ -23,7 +23,6 @@ const StudioNameWidget = ({
   popOver,
   onClick,
 }: IWidget) => {
-  // TODO
   const avatar = <Avatar size="small">{data?.nickName?.slice(0, 1)}</Avatar>;
   return (
     <StudioCard popOver={popOver} studio={data}>

+ 1 - 1
dashboard/src/components/blog/BlogNav.tsx

@@ -11,7 +11,7 @@ interface IWidgetBlogNav {
 const BlogNavWidget = ({ selectedKey, studio }: IWidgetBlogNav) => {
   //Library head bar
   const intl = useIntl(); //i18n
-  // TODO
+  // TODO 换图标
 
   const items: MenuProps["items"] = [
     {

+ 7 - 1
dashboard/src/components/blog/TimeLine.tsx

@@ -63,7 +63,13 @@ const TimeLineWidget = ({ studioName }: IWidget) => {
               style={{ backgroundColor: "unset" }}
               key={id}
               dot={icon}
-              label={<TimeShow time={item.date} showIcon={false} />}
+              label={
+                <TimeShow
+                  createdAt={item.date}
+                  showIcon={false}
+                  showLabel={false}
+                />
+              }
             >
               {intl.formatMessage({
                 id: `labels.${item.event}`,

+ 291 - 279
dashboard/src/components/channel/ChannelPickerTable.tsx

@@ -8,6 +8,7 @@ import {
   EditOutlined,
   MoreOutlined,
   CopyOutlined,
+  ReloadOutlined,
 } from "@ant-design/icons";
 
 import { IApiResponseChannelList, IFinal, TChannelType } from "../api/Channel";
@@ -24,8 +25,13 @@ import { currentUser as _currentUser } from "../../reducers/current-user";
 
 const { Link } = Typography;
 
+interface IParams {
+  owner?: string;
+}
+
 interface IProgressRequest {
   sentence: string[];
+  owner?: string;
 }
 export interface IItem {
   id: number;
@@ -37,9 +43,9 @@ export interface IItem {
   shareType: string;
   role?: string;
   publicity: number;
-  createdAt: number;
   final?: IFinal[];
   progress: number;
+  createdAt: number;
 }
 interface IWidget {
   type?: ArticleType | "editable";
@@ -47,6 +53,7 @@ interface IWidget {
   multiSelect?: boolean /*是否支持多选*/;
   selectedKeys?: string[];
   reload?: boolean;
+  disableChannelId?: string;
   onSelect?: Function;
 }
 const ChannelPickerTableWidget = ({
@@ -55,12 +62,15 @@ const ChannelPickerTableWidget = ({
   multiSelect = true,
   selectedKeys = [],
   onSelect,
+  disableChannelId,
   reload = false,
 }: IWidget) => {
   const intl = useIntl();
   const [selectedRowKeys, setSelectedRowKeys] =
     useState<React.Key[]>(selectedKeys);
   const [showCheckBox, setShowCheckBox] = useState<boolean>(false);
+  const [copyChannel, setCopyChannel] = useState<IChannel>();
+  const [copyOpen, setCopyOpen] = useState<boolean>(false);
   const user = useAppSelector(_currentUser);
   const ref = useRef<ActionType>();
 
@@ -71,304 +81,306 @@ const ChannelPickerTableWidget = ({
   }, [reload]);
 
   return (
-    <ProList<IItem>
-      actionRef={ref}
-      rowSelection={
-        showCheckBox
-          ? {
-              // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-              // 注释该行则默认不显示下拉选项
-              alwaysShowAlert: true,
-              selectedRowKeys: selectedRowKeys,
-              onChange: (selectedRowKeys: React.Key[]) => {
-                setSelectedRowKeys(selectedRowKeys);
-              },
-              selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-            }
-          : undefined
-      }
-      tableAlertRender={
-        showCheckBox
-          ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
-              console.log(selectedRowKeys);
-              return (
-                <Space>
-                  {intl.formatMessage({ id: "buttons.selected" })}
-                  <Badge color="geekblue" count={selectedRowKeys.length} />
-                  <Link onClick={onCleanSelected}>
-                    {intl.formatMessage({ id: "buttons.empty" })}
-                  </Link>
-                </Space>
-              );
-            }
-          : undefined
-      }
-      tableAlertOptionRender={
-        showCheckBox
-          ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
-              return (
-                <Space>
-                  <Link
-                    onClick={() => {
-                      console.log("select", selectedRowKeys);
-                      if (typeof onSelect !== "undefined") {
-                        onSelect(
-                          selectedRows.map((item) => {
-                            return {
-                              id: item.uid,
-                              name: item.title,
-                            };
-                          })
-                        );
+    <>
+      <ProList<IItem, IParams>
+        actionRef={ref}
+        rowSelection={
+          showCheckBox
+            ? {
+                // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+                // 注释该行则默认不显示下拉选项
+                alwaysShowAlert: true,
+                selectedRowKeys: selectedRowKeys,
+                onChange: (selectedRowKeys: React.Key[]) => {
+                  setSelectedRowKeys(selectedRowKeys);
+                },
+                selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+              }
+            : undefined
+        }
+        tableAlertRender={
+          showCheckBox
+            ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
+                return (
+                  <Space>
+                    {intl.formatMessage({ id: "buttons.selected" })}
+                    <Badge color="geekblue" count={selectedRowKeys.length} />
+                    <Link onClick={onCleanSelected}>
+                      {intl.formatMessage({ id: "buttons.empty" })}
+                    </Link>
+                  </Space>
+                );
+              }
+            : undefined
+        }
+        tableAlertOptionRender={
+          showCheckBox
+            ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
+                return (
+                  <Space>
+                    <Link
+                      onClick={() => {
+                        if (typeof onSelect !== "undefined") {
+                          onSelect(
+                            selectedRows.map((item) => {
+                              return {
+                                id: item.uid,
+                                name: item.title,
+                              };
+                            })
+                          );
+                          setShowCheckBox(false);
+                          ref.current?.reload();
+                        }
+                      }}
+                    >
+                      {intl.formatMessage({
+                        id: "buttons.ok",
+                      })}
+                    </Link>
+                    <Link
+                      type="danger"
+                      onClick={() => {
                         setShowCheckBox(false);
-                        ref.current?.reload();
-                      }
-                    }}
-                  >
-                    {intl.formatMessage({
-                      id: "buttons.ok",
-                    })}
-                  </Link>
-                  <Link
-                    type="danger"
-                    onClick={() => {
-                      setShowCheckBox(false);
-                    }}
-                  >
-                    {intl.formatMessage({
-                      id: "buttons.cancel",
-                    })}
-                  </Link>
-                </Space>
-              );
-            }
-          : undefined
-      }
-      request={async (params = {}, sorter, filter) => {
-        console.log(params, sorter, filter);
-        const sentElement = document.querySelectorAll(".pcd_sent");
-        let sentList: string[] = [];
-        for (let index = 0; index < sentElement.length; index++) {
-          const element = sentElement[index];
-          const id = element.id.split("_")[1];
-          sentList.push(id);
+                      }}
+                    >
+                      {intl.formatMessage({
+                        id: "buttons.cancel",
+                      })}
+                    </Link>
+                  </Space>
+                );
+              }
+            : undefined
         }
-        console.log("sentList", sentList);
-        const res = await post<IProgressRequest, IApiResponseChannelList>(
-          `/v2/channel-progress`,
-          {
-            sentence: sentList,
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          const sentElement = document.querySelectorAll(".pcd_sent");
+          let sentList: string[] = [];
+          for (let index = 0; index < sentElement.length; index++) {
+            const element = sentElement[index];
+            const id = element.id.split("_")[1];
+            sentList.push(id);
           }
-        );
-        console.log("progress data", res.data.rows);
-        const items: IItem[] = res.data.rows
-          .filter((value) => value.name.substring(0, 4) !== "_Sys")
-          .map((item, id) => {
-            const date = new Date(item.created_at);
-            let all: number = 0;
-            let finished: number = 0;
-            item.final?.forEach((value) => {
-              all += value[0];
-              finished += value[1] ? value[0] : 0;
+          const res = await post<IProgressRequest, IApiResponseChannelList>(
+            `/v2/channel-progress`,
+            {
+              sentence: sentList,
+              owner: params.owner,
+            }
+          );
+          console.log("progress data", res.data.rows);
+          const items: IItem[] = res.data.rows
+            .filter((value) => value.name.substring(0, 4) !== "_Sys")
+            .map((item, id) => {
+              const date = new Date(item.created_at);
+              let all: number = 0;
+              let finished: number = 0;
+              item.final?.forEach((value) => {
+                all += value[0];
+                finished += value[1] ? value[0] : 0;
+              });
+              const progress = finished / all;
+              return {
+                id: id,
+                uid: item.uid,
+                title: item.name,
+                summary: item.summary,
+                studio: item.studio,
+                shareType: "my",
+                role: item.role,
+                type: item.type,
+                publicity: item.status,
+                createdAt: date.getTime(),
+                final: item.final,
+                progress: progress,
+              };
             });
-            const progress = finished / all;
-            return {
-              id: id,
-              uid: item.uid,
-              title: item.name,
-              summary: item.summary,
-              studio: item.studio,
-              shareType: "my",
-              role: item.role,
-              type: item.type,
-              publicity: item.status,
-              createdAt: date.getTime(),
-              final: item.final,
-              progress: progress,
-            };
-          });
-        //当前被选择的
-        const currChannel = items.filter((value) =>
-          selectedRowKeys.includes(value.uid)
-        );
-        let show = selectedRowKeys;
-        //有进度的
-        const progressing = items.filter(
-          (value) => value.progress > 0 && !show.includes(value.uid)
-        );
-        show = [...show, ...progressing.map((item) => item.uid)];
-        //我自己的
-        const myChannel = items.filter(
-          (value) => value.role === "owner" && !show.includes(value.uid)
-        );
-        show = [...show, ...myChannel.map((item) => item.uid)];
-        //其他的
-        const others = items.filter(
-          (value) => !show.includes(value.uid) && value.role !== "member"
-        );
-        console.log("user:", user);
-        setSelectedRowKeys(selectedRowKeys);
-        const channelData = [
-          ...currChannel,
-          ...progressing,
-          ...myChannel,
-          ...others,
-        ];
-        console.log("channel list ", channelData);
-        return {
-          total: res.data.count,
-          succcess: true,
-          data: channelData,
-        };
-      }}
-      rowKey="uid"
-      bordered
-      options={false}
-      search={{
-        filterType: "light",
-      }}
-      toolBarRender={() => [
-        <Button
-          onClick={() => {
-            ref.current?.reload();
-          }}
-        >
-          reload
-        </Button>,
-        multiSelect ? (
+          //当前被选择的
+          const currChannel = items.filter((value) =>
+            selectedRowKeys.includes(value.uid)
+          );
+          let show = selectedRowKeys;
+          //有进度的
+          const progressing = items.filter(
+            (value) => value.progress > 0 && !show.includes(value.uid)
+          );
+          show = [...show, ...progressing.map((item) => item.uid)];
+          //我自己的
+          const myChannel = items.filter(
+            (value) => value.role === "owner" && !show.includes(value.uid)
+          );
+          show = [...show, ...myChannel.map((item) => item.uid)];
+          //其他的
+          const others = items.filter(
+            (value) => !show.includes(value.uid) && value.role !== "member"
+          );
+          setSelectedRowKeys(selectedRowKeys);
+          const channelData = [
+            ...currChannel,
+            ...progressing,
+            ...myChannel,
+            ...others,
+          ];
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: channelData,
+          };
+        }}
+        rowKey="uid"
+        bordered
+        options={false}
+        search={{
+          filterType: "light",
+        }}
+        toolBarRender={() => [
+          multiSelect ? (
+            <Button
+              onClick={() => {
+                setShowCheckBox(true);
+              }}
+            >
+              选择
+            </Button>
+          ) : undefined,
           <Button
+            type="link"
             onClick={() => {
-              setShowCheckBox(true);
-              console.log("user:", user);
+              ref.current?.reload();
             }}
-          >
-            选择
-          </Button>
-        ) : undefined,
-      ]}
-      metas={{
-        title: {
-          render(dom, entity, index, action, schema) {
-            let pIcon = <></>;
-            switch (entity.publicity) {
-              case 10:
-                pIcon = <LockIcon />;
-                break;
-              case 30:
-                pIcon = <GlobalOutlined />;
-                break;
-            }
+            icon={<ReloadOutlined />}
+          />,
+        ]}
+        metas={{
+          title: {
+            render(dom, entity, index, action, schema) {
+              let pIcon = <></>;
+              switch (entity.publicity) {
+                case 10:
+                  pIcon = <LockIcon />;
+                  break;
+                case 30:
+                  pIcon = <GlobalOutlined />;
+                  break;
+              }
 
-            return (
-              <div
-                key={index}
-                style={{
-                  width: "100%",
-                  borderRadius: 5,
-                  padding: "0 5px",
-                  background:
-                    selectedKeys.includes(entity.uid) && !showCheckBox
-                      ? "linear-gradient(to right,#006112,rgba(0,0,0,0))"
-                      : undefined,
-                }}
-              >
-                <div key="info" style={{ overflowX: "clip", display: "flex" }}>
-                  <Space>
-                    {pIcon}
-                    {entity.role !== "member" ? <EditOutlined /> : undefined}
-                  </Space>
-                  <Button
-                    type="link"
-                    onClick={() => {
-                      if (typeof onSelect !== "undefined") {
-                        const e: IChannel = {
-                          name: entity.title,
-                          id: entity.uid,
-                        };
-                        onSelect([e]);
-                      }
-                    }}
+              return (
+                <div
+                  key={index}
+                  style={{
+                    width: "100%",
+                    borderRadius: 5,
+                    padding: "0 5px",
+                    background:
+                      selectedKeys.includes(entity.uid) && !showCheckBox
+                        ? "linear-gradient(to left, rgb(63 255 165 / 54%), rgba(0, 0, 0, 0))"
+                        : undefined,
+                  }}
+                >
+                  <div
+                    key="info"
+                    style={{ overflowX: "clip", display: "flex" }}
                   >
                     <Space>
-                      <StudioName data={entity.studio} showName={false} />
-                      {entity.title}
+                      {pIcon}
+                      {entity.role !== "member" ? <EditOutlined /> : undefined}
                     </Space>
-                  </Button>
-                </div>
-                <div key="progress">
-                  <ProgressSvg data={entity.final} width={200} />
+                    <Button
+                      type="link"
+                      disabled={disableChannelId === entity.uid}
+                      onClick={() => {
+                        if (typeof onSelect !== "undefined") {
+                          const e: IChannel = {
+                            name: entity.title,
+                            id: entity.uid,
+                          };
+                          onSelect([e]);
+                        }
+                      }}
+                    >
+                      <Space>
+                        <StudioName data={entity.studio} showName={false} />
+                        {entity.title}
+                      </Space>
+                    </Button>
+                  </div>
+                  <div key="progress">
+                    <ProgressSvg data={entity.final} width={200} />
+                  </div>
                 </div>
-              </div>
-            );
+              );
+            },
+            search: false,
           },
-          search: false,
-        },
-        actions: {
-          render: (dom, entity, index, action, schema) => {
-            return (
-              <Dropdown
-                key={index}
-                trigger={["click"]}
-                menu={{
-                  items: [
-                    {
-                      key: "copy_to",
-                      label: (
-                        <CopyToModal
-                          trigger={intl.formatMessage({
-                            id: "buttons.copy.to",
-                          })}
-                          channel={{
+          actions: {
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <Dropdown
+                  key={index}
+                  trigger={["click"]}
+                  menu={{
+                    items: [
+                      {
+                        key: "copy-to",
+                        label: intl.formatMessage({
+                          id: "buttons.copy.to",
+                        }),
+                        icon: <CopyOutlined />,
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "copy-to":
+                          setCopyChannel({
                             id: entity.uid,
                             name: entity.title,
                             type: entity.type,
-                          }}
-                        />
-                      ),
-                      icon: <CopyOutlined />,
-                    },
-                  ],
-                  onClick: (e) => {
-                    console.log("click ", e);
-                    switch (e.key) {
-                      case "copy_to":
-                        break;
+                          });
+                          setCopyOpen(true);
+                          break;
 
-                      default:
-                        break;
-                    }
-                  },
-                }}
-                placement="bottomRight"
-              >
-                <Button
-                  type="link"
-                  size="small"
-                  icon={<MoreOutlined />}
-                ></Button>
-              </Dropdown>
-            );
-          },
-        },
-        status: {
-          // 自己扩展的字段,主要用于筛选,不在列表中显示
-          title: "版本筛选",
-          valueType: "select",
-          valueEnum: {
-            all: { text: "全部", status: "Default" },
-            my: {
-              text: "我的",
-            },
-            closed: {
-              text: "协作",
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                  placement="bottomRight"
+                >
+                  <Button
+                    type="link"
+                    size="small"
+                    icon={<MoreOutlined />}
+                  ></Button>
+                </Dropdown>
+              );
             },
-            processing: {
-              text: "社区公开",
+          },
+          owner: {
+            // 自己扩展的字段,主要用于筛选,不在列表中显示
+            title: "版本筛选",
+            valueType: "select",
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              my: {
+                text: "我的",
+              },
+              cooperator: {
+                text: "协作",
+              },
+              public: {
+                text: "社区公开",
+              },
             },
           },
-        },
-      }}
-    />
+        }}
+      />
+      <CopyToModal
+        channel={copyChannel}
+        open={copyOpen}
+        onClose={() => setCopyOpen(false)}
+      />
+    </>
   );
 };
 

+ 26 - 13
dashboard/src/components/channel/ChannelTable.tsx

@@ -1,8 +1,7 @@
-import { useParams } from "react-router-dom";
 import { ActionType, ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
-import { Badge, message, Modal, Typography } from "antd";
+import { Alert, Badge, message, Modal, Typography } from "antd";
 import { Button, Dropdown, Popover } from "antd";
 import {
   PlusOutlined,
@@ -25,8 +24,9 @@ import ShareModal from "../../components/share/ShareModal";
 import { EResType } from "../../components/share/Share";
 import StudioName, { IStudio } from "../../components/auth/StudioName";
 import StudioSelect from "../../components/channel/StudioSelect";
-import { ArticleType } from "../article/Article";
 import { IChannel } from "./Channel";
+import { getSorterUrl } from "../../utils";
+
 const { Text } = Typography;
 
 export interface IResNumberResponse {
@@ -61,19 +61,21 @@ interface IChannelItem {
   role?: TRole;
   studio?: IStudio;
   publicity: number;
-  createdAt: number;
+  created_at: string;
 }
 
 interface IWidget {
   studioName?: string;
   type?: string;
   disableChannels?: string[];
+  channelType?: TChannelType;
   onSelect?: Function;
 }
 
 const ChannelTableWidget = ({
   studioName,
   disableChannels,
+  channelType,
   type,
   onSelect,
 }: IWidget) => {
@@ -143,6 +145,13 @@ const ChannelTableWidget = ({
 
   return (
     <>
+      {channelType ? (
+        <Alert
+          message={`仅显示版本类型${channelType}`}
+          type="success"
+          closable
+        />
+      ) : undefined}
       <ProTable<IChannelItem>
         actionRef={ref}
         columns={[
@@ -309,12 +318,12 @@ const ChannelTableWidget = ({
             title: intl.formatMessage({
               id: "forms.fields.created-at.label",
             }),
-            key: "created-at",
+            key: "created_at",
             width: 100,
             search: false,
-            dataIndex: "createdAt",
+            dataIndex: "created_at",
             valueType: "date",
-            sorter: (a, b) => a.createdAt - b.createdAt,
+            sorter: true,
           },
           {
             title: intl.formatMessage({ id: "buttons.option" }),
@@ -362,9 +371,9 @@ const ChannelTableWidget = ({
                     },
                   }}
                 >
-                  <Link to={`/studio/${studioName}/channel/${row.uid}/edit`}>
+                  <Link to={`/studio/${studioName}/channel/${row.uid}/setting`}>
                     {intl.formatMessage({
-                      id: "buttons.edit",
+                      id: "buttons.setting",
                     })}
                   </Link>
                 </Dropdown.Button>,
@@ -410,16 +419,20 @@ const ChannelTableWidget = ({
         }}
         */
         request={async (params = {}, sorter, filter) => {
-          // TODO 分页
           console.log(params, sorter, filter);
           let url = `/v2/channel?view=studio&view2=${activeKey}&name=${studioName}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+
           url += collaborator ? "&collaborator=" + collaborator : "";
           url += params.keyword ? "&search=" + params.keyword : "";
-
+          url += channelType ? "&type=" + channelType : "";
+          url += getSorterUrl(sorter);
           console.log("url", url);
           const res: IApiResponseChannelList = await get(url);
           const items: IChannelItem[] = res.data.rows.map((item, id) => {
-            const date = new Date(item.created_at);
             return {
               id: id + 1,
               uid: item.uid,
@@ -429,7 +442,7 @@ const ChannelTableWidget = ({
               role: item.role,
               studio: item.studio,
               publicity: item.status,
-              createdAt: date.getTime(),
+              created_at: item.created_at,
             };
           });
           return {

+ 4 - 0
dashboard/src/components/channel/ChannelTableModal.tsx

@@ -6,9 +6,11 @@ import ChannelTable from "./ChannelTable";
 import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
 import { IChannel } from "./Channel";
+import { TChannelType } from "../api/Channel";
 
 interface IWidget {
   trigger?: React.ReactNode;
+  channelType?: TChannelType;
   type?: ArticleType | "editable";
   articleId?: string;
   multiSelect?: boolean;
@@ -23,6 +25,7 @@ const ChannelTableModalWidget = ({
   articleId,
   multiSelect = true,
   disableChannels,
+  channelType,
   open = false,
   onClose,
   onSelect,
@@ -65,6 +68,7 @@ const ChannelTableModalWidget = ({
         <ChannelTable
           studioName={user?.realName}
           type={type}
+          channelType={channelType}
           disableChannels={disableChannels}
           onSelect={(channel: IChannel) => {
             handleCancel();

+ 9 - 11
dashboard/src/components/channel/ChapterInChannelList.tsx

@@ -22,14 +22,14 @@ interface IItem {
   path: string;
   progress: number;
   view: number;
-  createdAt: number;
-  updatedAt: number;
+  created_at: string;
+  updated_at: string;
 }
 interface IWidget {
   channelId?: string;
   onSelect?: Function;
 }
-const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
+const ChapterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
   const intl = useIntl();
 
   return (
@@ -118,9 +118,9 @@ const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
           key: "created-at",
           width: 100,
           search: false,
-          dataIndex: "createdAt",
+          dataIndex: "created_at",
           valueType: "date",
-          sorter: (a, b) => a.createdAt - b.createdAt,
+          sorter: false,
         },
         {
           title: intl.formatMessage({ id: "buttons.option" }),
@@ -167,7 +167,7 @@ const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
         },
       ]}
       request={async (params = {}, sorter, filter) => {
-        // TODO
+        // TODO 加排序
         console.log(params, sorter, filter);
         const offset = (params.current || 1 - 1) * (params.pageSize || 20);
         const res = await get<IChapterListResponse>(
@@ -175,8 +175,6 @@ const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
         );
         console.log(res.data.rows);
         const items: IItem[] = res.data.rows.map((item, id) => {
-          const createdAt = new Date(item.created_at);
-          const updatedAt = new Date(item.updated_at);
           return {
             sn: id + offset + 1,
             book: item.book,
@@ -187,8 +185,8 @@ const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
             summary: item.summary,
             path: item.path,
             progress: item.progress,
-            createdAt: createdAt.getTime(),
-            updatedAt: updatedAt.getTime(),
+            created_at: item.created_at,
+            updated_at: item.updated_at,
           };
         });
         return {
@@ -211,4 +209,4 @@ const ChpaterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
   );
 };
 
-export default ChpaterInChannelListWidget;
+export default ChapterInChannelListWidget;

+ 16 - 6
dashboard/src/components/channel/CopyToModal.tsx

@@ -1,28 +1,38 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { Modal } from "antd";
 
 import CopyToStep from "./CopyToStep";
 import { IChannel } from "./Channel";
 
 interface IWidget {
-  trigger: JSX.Element | string;
+  trigger?: JSX.Element | string;
   channel?: IChannel;
+  open?: boolean;
+  onClose?: Function;
 }
-const CopyToModalWidget = ({ trigger, channel }: IWidget) => {
-  const [isModalOpen, setIsModalOpen] = useState(false);
+const CopyToModalWidget = ({ trigger, channel, open, onClose }: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(open);
   const [initStep, setInitStep] = useState(0);
 
+  useEffect(() => setIsModalOpen(open), [open]);
+
   const showModal = () => {
     setIsModalOpen(true);
     setInitStep(0);
   };
 
-  const handleOk = () => {
+  const modalClose = () => {
     setIsModalOpen(false);
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
+  };
+  const handleOk = () => {
+    modalClose();
   };
 
   const handleCancel = () => {
-    setIsModalOpen(false);
+    modalClose();
   };
 
   return (

+ 1 - 0
dashboard/src/components/channel/CopyToStep.tsx

@@ -62,6 +62,7 @@ const CopyToStepWidget = ({
         <div style={contentStyle}>
           <ChannelPickerTable
             type="editable"
+            disableChannelId={channel?.id}
             multiSelect={false}
             onSelect={(e: IChannel[]) => {
               console.log("channel", e);

+ 84 - 0
dashboard/src/components/channel/Edit.tsx

@@ -0,0 +1,84 @@
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { IApiResponseChannel } from "../../components/api/Channel";
+import { get, put } from "../../request";
+import ChannelTypeSelect from "../../components/channel/ChannelTypeSelect";
+import LangSelect from "../../components/general/LangSelect";
+import PublicitySelect from "../../components/studio/PublicitySelect";
+
+interface IFormData {
+  name: string;
+  type: string;
+  lang: string;
+  summary: string;
+  status: number;
+  studio: string;
+}
+interface IWidget {
+  studioName?: string;
+  channelId?: string;
+  onLoad?: Function;
+}
+const EditWidget = ({ studioName, channelId, onLoad }: IWidget) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        console.log(values);
+        const res = await put(`/v2/channel/${channelId}`, values);
+        console.log(res);
+        message.success(intl.formatMessage({ id: "flashes.success" }));
+      }}
+      formKey="channel_edit"
+      request={async () => {
+        const res = await get<IApiResponseChannel>(`/v2/channel/${channelId}`);
+        if (typeof onLoad !== "undefined") {
+          onLoad(res.data);
+        }
+        return {
+          name: res.data.name,
+          type: res.data.type,
+          lang: res.data.lang,
+          summary: res.data.summary,
+          status: res.data.status,
+          studio: studioName ? studioName : "",
+        };
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "channel.name" })}
+          rules={[
+            {
+              required: true,
+            },
+          ]}
+        />
+      </ProForm.Group>
+
+      <ProForm.Group>
+        <ChannelTypeSelect />
+        <LangSelect />
+      </ProForm.Group>
+      <ProForm.Group>
+        <PublicitySelect />
+      </ProForm.Group>
+
+      <ProForm.Group>
+        <ProFormTextArea width="md" name="summary" label="简介" />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default EditWidget;

+ 0 - 1
dashboard/src/components/corpus/BookTree.tsx

@@ -81,7 +81,6 @@ const BookTreeWidget = ({
     });
   }
 
-  // TODO
   return (
     <Space direction="vertical" style={{ padding: 10, width: "100%" }}>
       <Space style={{ display: "flex", justifyContent: "space-between" }}>

+ 1 - 6
dashboard/src/components/corpus/ChapterCard.tsx

@@ -84,12 +84,7 @@ const ChapterCardWidget = ({ data, onTagClick }: IWidget) => {
           </div>
           <Space>
             <ChannelListItem channel={data.channel} studio={data.studio} />
-            <TimeShow
-              time={data.updatedAt}
-              title={intl.formatMessage({
-                id: "labels.updated-at",
-              })}
-            />
+            <TimeShow updatedAt={data.updatedAt} />
           </Space>
         </div>
       </Col>

+ 1 - 1
dashboard/src/components/corpus/ChapterChannelSelect.tsx

@@ -91,7 +91,7 @@ const ChapterChannelSelectWidget = ({
                   <EyeOutlined />
                   {item.hit} | <LikeOutlined />
                   {item.like} |
-                  <TimeShow time={item.updatedAt} title={item.updatedAt} />
+                  <TimeShow updatedAt={item.updatedAt} />
                 </Space>
               </Text>
             </List.Item>

+ 1 - 1
dashboard/src/components/corpus/ChapterInChannel.tsx

@@ -96,7 +96,7 @@ const ChapterInChannelWidget = ({
                     <EyeOutlined />
                     {item.hit} | <LikeOutlined />
                     {item.like} |
-                    <TimeShow time={item.updatedAt} /> |
+                    <TimeShow updatedAt={item.updatedAt} /> |
                     <ProgressOutlinedIcon />
                     {`${item.progress}%`}
                   </Space>

+ 11 - 8
dashboard/src/components/corpus/PaliChapterListByTag.tsx

@@ -5,18 +5,21 @@ import { IPaliChapterListResponse } from "../api/Corpus";
 import { IPaliChapterData } from "./PaliChapterCard";
 import PaliChapterList, { IChapterClickEvent } from "./PaliChapterList";
 
-interface IWidgetPaliChapterListByTag {
+interface IWidget {
   tag: string[];
   onChapterClick?: Function;
 }
 
-const PaliChapterListByTagWidget = (prop: IWidgetPaliChapterListByTag) => {
+const PaliChapterListByTagWidget = ({ tag = [], onChapterClick }: IWidget) => {
   const [tableData, setTableData] = useState<IPaliChapterData[]>([]);
 
   useEffect(() => {
-    console.log("palichapterlist useEffect");
-    let url = `/v2/palitext?view=chapter&tags=${prop.tag.join()}`;
-    console.log("tag url", url);
+    if (tag.length === 0) {
+      setTableData([]);
+      return;
+    }
+    let url = `/v2/palitext?view=chapter&tags=${tag.join()}`;
+    console.log("url", url);
     get<IPaliChapterListResponse>(url).then((json) => {
       if (json.ok) {
         let newTree: IPaliChapterData[] = json.data.rows.map((item) => {
@@ -37,15 +40,15 @@ const PaliChapterListByTagWidget = (prop: IWidgetPaliChapterListByTag) => {
         console.error(json.message);
       }
     });
-  }, [prop.tag]);
+  }, [tag]);
 
   return (
     <PaliChapterList
       data={tableData}
       maxLevel={1}
       onChapterClick={(e: IChapterClickEvent) => {
-        if (typeof prop.onChapterClick !== "undefined") {
-          prop.onChapterClick(e);
+        if (typeof onChapterClick !== "undefined") {
+          onChapterClick(e);
         }
       }}
     />

+ 1 - 7
dashboard/src/components/corpus/SentHistory.tsx

@@ -91,13 +91,7 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
         },
         description: {
           render: (text, row, index, action) => {
-            return (
-              <TimeShow
-                type="secondary"
-                time={row.createdAt}
-                title="created at"
-              />
-            );
+            return <TimeShow type="secondary" createdAt={row.createdAt} />;
           },
         },
         actions: {

+ 4 - 7
dashboard/src/components/corpus/TopChapter.tsx

@@ -18,8 +18,8 @@ interface IItem {
   channelId: string;
   progress: number;
   view: number;
-  createdAt: number;
-  updatedAt: number;
+  created_at: string;
+  updated_at: string;
 }
 interface IWidget {
   studioName?: string;
@@ -61,7 +61,6 @@ const TopChapterWidget = ({ studioName }: IWidget) => {
       showActions="hover"
       grid={{ gutter: 16, column: 2, md: 1 }}
       request={async (params = {}, sorter, filter) => {
-        // TODO
         console.log(params, sorter, filter);
         const offset = (params.current || 1 - 1) * (params.pageSize || 20);
         const res = await get<IChapterListResponse>(
@@ -69,8 +68,6 @@ const TopChapterWidget = ({ studioName }: IWidget) => {
         );
         console.log(res.data.rows);
         const items: IItem[] = res.data.rows.map((item, id) => {
-          const createdAt = new Date(item.created_at);
-          const updatedAt = new Date(item.updated_at);
           return {
             sn: id + offset + 1,
             book: item.book,
@@ -82,8 +79,8 @@ const TopChapterWidget = ({ studioName }: IWidget) => {
             channelId: item.channel_id,
             path: item.path,
             progress: item.progress,
-            createdAt: createdAt.getTime(),
-            updatedAt: updatedAt.getTime(),
+            created_at: item.created_at,
+            updated_at: item.updated_at,
           };
         });
         return {

+ 0 - 1
dashboard/src/components/course/AddLesson.tsx

@@ -19,7 +19,6 @@ const AddLessonWidget = ({ groupId }: IWidget) => {
   const form = (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
-        // TODO
         console.log(values);
         message.success(intl.formatMessage({ id: "flashes.success" }));
       }}

+ 0 - 1
dashboard/src/components/course/AddMember.tsx

@@ -24,7 +24,6 @@ const AddMemeberWidget = ({ courseId, onCreated }: IWidget) => {
   const form = (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
-        // TODO
         console.log(values);
         if (typeof courseId !== "undefined") {
           post<ICourseMemberData, ICourseMemberResponse>("/v2/course-member", {

+ 0 - 1
dashboard/src/components/course/AddStudent.tsx

@@ -20,7 +20,6 @@ const AddStudentWidget = ({ courseId }: IWidget) => {
   const form = (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
-        // TODO
         console.log(values);
         message.success(intl.formatMessage({ id: "flashes.success" }));
       }}

+ 0 - 1
dashboard/src/components/course/CourseMember.tsx

@@ -68,7 +68,6 @@ const CourseMemberWidget = ({ courseId }: IWidget) => {
         }}
         showActions="hover"
         request={async (params = {}, sorter, filter) => {
-          // TODO
           console.log(params, sorter, filter);
 
           let url = `/v2/course-member?view=course&id=${courseId}`;

+ 1 - 1
dashboard/src/components/dict/DictCreate.tsx

@@ -29,7 +29,7 @@ const DictCreateWidget = (prop: IWidgetDictCreate) => {
     <>
       <ProForm<IDictFormData>
         onFinish={async (values: IDictFormData) => {
-          // TODO
+          // TODO 是否要删掉?
           console.log(values);
           message.success(intl.formatMessage({ id: "flashes.success" }));
         }}

+ 0 - 1
dashboard/src/components/dict/DictEdit.tsx

@@ -18,7 +18,6 @@ const DictEditWidget = ({ wordId }: IWidget) => {
     <>
       <ProForm<IDictFormData>
         onFinish={async (values: IDictFormData) => {
-          // TODO
           console.log(values);
           const request: IDictRequest = {
             id: values.id,

+ 8 - 0
dashboard/src/components/dict/SelectCase.tsx

@@ -59,6 +59,10 @@ const SelectCaseWidget = ({ value, onCaseChange }: IWidget) => {
       value: "voc",
       label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
     },
+    {
+      value: "?",
+      label: intl.formatMessage({ id: "dict.fields.type.?.label" }),
+    },
   ];
   const case2 = [
     {
@@ -71,6 +75,10 @@ const SelectCaseWidget = ({ value, onCaseChange }: IWidget) => {
       label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
       children: case8,
     },
+    {
+      value: "?",
+      label: intl.formatMessage({ id: "dict.fields.type.?.label" }),
+    },
   ];
   const case3 = [
     {

+ 8 - 2
dashboard/src/components/dict/UserDictList.tsx

@@ -26,8 +26,8 @@ import { delete_2, get } from "../../request";
 import { useRef, useState } from "react";
 import DictEdit from "../../components/dict/DictEdit";
 import { IDeleteResponse } from "../../components/api/Article";
-import { getSorterUrl } from "../../pages/admin/relation/list";
 import TimeShow from "../general/TimeShow";
+import { getSorterUrl } from "../../utils";
 
 const { Link } = Typography;
 
@@ -209,7 +209,13 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
             valueType: "date",
             sorter: true,
             render: (text, row, index, action) => {
-              return <TimeShow time={row.updated_at} showIcon={false} />;
+              return (
+                <TimeShow
+                  updatedAt={row.updated_at}
+                  showIcon={false}
+                  showLabel={false}
+                />
+              );
             },
           },
           {

+ 24 - 14
dashboard/src/components/discussion/DiscussionAnchor.tsx

@@ -1,3 +1,4 @@
+import { Skeleton } from "antd";
 import { useEffect, useState } from "react";
 import { get } from "../../request";
 import { IArticleResponse } from "../api/Article";
@@ -14,6 +15,7 @@ interface IWidget {
 }
 const DiscussionAnchorWidget = ({ resId, resType, topicId }: IWidget) => {
   const [content, setContent] = useState<string>();
+  const [loading, setLoading] = useState(true);
   useEffect(() => {
     if (typeof topicId === "string") {
       get<ICommentAnchorResponse>(`/v2/discussion-anchor/${topicId}`).then(
@@ -30,19 +32,23 @@ const DiscussionAnchorWidget = ({ resId, resType, topicId }: IWidget) => {
   useEffect(() => {
     switch (resType) {
       case "sentence":
-        get<ISentenceResponse>(`/v2/sentence/${resId}`).then((json) => {
-          if (json.ok) {
-            const id = `${json.data.book}-${json.data.paragraph}-${json.data.word_start}-${json.data.word_end}`;
-            const channel = json.data.channel.id;
-            const url = `/v2/corpus-sent/${id}?mode=edit&channels=${channel}`;
-            console.log("url", url);
-            get<IArticleResponse>(url).then((json) => {
-              if (json.ok) {
-                setContent(json.data.content);
-              }
-            });
-          }
-        });
+        const url = `/v2/sentence/${resId}`;
+        console.log("url", url);
+        get<ISentenceResponse>(url)
+          .then((json) => {
+            if (json.ok) {
+              const id = `${json.data.book}-${json.data.paragraph}-${json.data.word_start}-${json.data.word_end}`;
+              const channel = json.data.channel.id;
+              const url = `/v2/corpus-sent/${id}?mode=edit&channels=${channel}`;
+              console.log("url", url);
+              get<IArticleResponse>(url).then((json) => {
+                if (json.ok) {
+                  setContent(json.data.content);
+                }
+              });
+            }
+          })
+          .finally(() => setLoading(false));
         break;
       default:
         break;
@@ -50,7 +56,11 @@ const DiscussionAnchorWidget = ({ resId, resType, topicId }: IWidget) => {
   }, [resId, resType]);
   return (
     <AnchorCard>
-      <MdView html={content} />
+      {loading ? (
+        <Skeleton title={{ width: 200 }} paragraph={{ rows: 4 }} active />
+      ) : (
+        <MdView html={content} />
+      )}
     </AnchorCard>
   );
 };

+ 10 - 5
dashboard/src/components/discussion/DiscussionBox.tsx

@@ -6,6 +6,7 @@ import DiscussionTopic from "./DiscussionTopic";
 import DiscussionListCard, { TResType } from "./DiscussionListCard";
 import { IComment } from "./DiscussionItem";
 import DiscussionAnchor from "./DiscussionAnchor";
+import { Link } from "react-router-dom";
 
 export interface IAnswerCount {
   id: string;
@@ -31,10 +32,7 @@ const DiscussionBoxWidget = ({
   const drawerMaxWidth = 1100;
 
   const [drawerWidth, setDrawerWidth] = useState(drawerMinWidth);
-  const showChildrenDrawer = (
-    e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
-    comment: IComment
-  ) => {
+  const showChildrenDrawer = (comment: IComment) => {
     setChildrenDrawer(true);
     setTopicComment(comment);
   };
@@ -53,6 +51,9 @@ const DiscussionBoxWidget = ({
         destroyOnClose
         extra={
           <Space>
+            <Link to={`/discussion/show/${resType}/${resId}`} target="_blank">
+              在新窗口打开
+            </Link>
             {drawerWidth === drawerMinWidth ? (
               <Button
                 type="link"
@@ -83,7 +84,11 @@ const DiscussionBoxWidget = ({
         <DiscussionListCard
           resId={resId}
           resType={resType}
-          onSelect={showChildrenDrawer}
+          onSelect={(
+            e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+            comment: IComment
+          ) => showChildrenDrawer(comment)}
+          onReply={(comment: IComment) => showChildrenDrawer(comment)}
           changedAnswerCount={answerCount}
           onItemCountChange={(count: number) => {
             if (typeof onCommentCountChange !== "undefined") {

+ 1 - 0
dashboard/src/components/discussion/DiscussionEdit.tsx

@@ -71,6 +71,7 @@ const DiscussionEditWidget = ({
                     parent: json.data.parent,
                     title: json.data.title,
                     content: json.data.content,
+                    status: json.data.status,
                     childrenCount: json.data.children_count,
                     createdAt: json.data.created_at,
                     updatedAt: json.data.updated_at,

+ 30 - 7
dashboard/src/components/discussion/DiscussionItem.tsx

@@ -13,6 +13,7 @@ export interface IComment {
   parent?: string | null;
   title?: string;
   content?: string;
+  status?: "active" | "close";
   children?: IComment[];
   childrenCount?: number;
   createdAt?: string;
@@ -20,21 +21,36 @@ export interface IComment {
 }
 interface IWidget {
   data: IComment;
+  isFocus?: boolean;
   onSelect?: Function;
   onCreated?: Function;
   onDelete?: Function;
+  onReply?: Function;
+  onClose?: Function;
 }
 const DiscussionItemWidget = ({
   data,
+  isFocus = false,
   onSelect,
   onCreated,
   onDelete,
+  onReply,
+  onClose,
 }: IWidget) => {
   const [edit, setEdit] = useState(false);
   const [currData, setCurrData] = useState<IComment>(data);
   return (
-    <div style={{ display: "flex", width: "100%" }}>
-      <div style={{ width: "2em" }}>
+    <div
+      id={`answer-${data.id}`}
+      style={{
+        display: "flex",
+        width: "100%",
+        border: isFocus ? "2px solid blue" : "unset",
+        borderRadius: 10,
+        padding: 5,
+      }}
+    >
+      <div style={{ width: "2em", display: "none" }}>
         <Avatar size="small">{data.user?.nickName?.slice(0, 1)}</Avatar>
       </div>
       <div style={{ width: "100%" }}>
@@ -58,12 +74,9 @@ const DiscussionItemWidget = ({
             onEdit={() => {
               setEdit(true);
             }}
-            onSelect={(
-              e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
-              data: IComment
-            ) => {
+            onSelect={(e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
               if (typeof onSelect !== "undefined") {
-                onSelect(e, data);
+                onSelect(e, currData);
               }
             }}
             onDelete={(id: string) => {
@@ -71,6 +84,16 @@ const DiscussionItemWidget = ({
                 onDelete();
               }
             }}
+            onReply={() => {
+              if (typeof onReply !== "undefined") {
+                onReply(currData);
+              }
+            }}
+            onClose={(value: boolean) => {
+              if (typeof onClose !== "undefined") {
+                onClose(value);
+              }
+            }}
           />
         )}
       </div>

+ 19 - 1
dashboard/src/components/discussion/DiscussionList.tsx

@@ -6,8 +6,16 @@ interface IWidget {
   data: IComment[];
   onSelect?: Function;
   onDelete?: Function;
+  onReply?: Function;
+  onClose?: Function;
 }
-const DiscussionListWidget = ({ data, onSelect, onDelete }: IWidget) => {
+const DiscussionListWidget = ({
+  data,
+  onSelect,
+  onDelete,
+  onReply,
+  onClose,
+}: IWidget) => {
   return (
     <List
       pagination={{
@@ -35,6 +43,16 @@ const DiscussionListWidget = ({ data, onSelect, onDelete }: IWidget) => {
                 onDelete(item.id);
               }
             }}
+            onReply={() => {
+              if (typeof onReply !== "undefined") {
+                onReply(item);
+              }
+            }}
+            onClose={() => {
+              if (typeof onClose !== "undefined") {
+                onClose(item);
+              }
+            }}
           />
         </List.Item>
       )}

+ 175 - 92
dashboard/src/components/discussion/DiscussionListCard.tsx

@@ -1,13 +1,21 @@
-import { useState, useEffect } from "react";
-import { useIntl } from "react-intl";
-import { Card, message, Skeleton, Typography } from "antd";
+import { useEffect, useRef, useState } from "react";
+
+import { Collapse, Typography } from "antd";
+
+import { get, put } from "../../request";
+import {
+  ICommentListResponse,
+  ICommentRequest,
+  ICommentResponse,
+} from "../api/Comment";
+
+import DiscussionItem, { IComment } from "./DiscussionItem";
 
-import { get } from "../../request";
-import { ICommentListResponse } from "../api/Comment";
-import CommentCreate from "./DiscussionCreate";
-import { IComment } from "./DiscussionItem";
-import DiscussionList from "./DiscussionList";
 import { IAnswerCount } from "./DiscussionBox";
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { renderBadge } from "../channel/ChannelTable";
+import DiscussionCreate from "./DiscussionCreate";
+const { Panel } = Collapse;
 
 export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
 interface IWidget {
@@ -17,6 +25,7 @@ interface IWidget {
   changedAnswerCount?: IAnswerCount;
   onSelect?: Function;
   onItemCountChange?: Function;
+  onReply?: Function;
 }
 const DiscussionListCardWidget = ({
   resId,
@@ -25,67 +34,19 @@ const DiscussionListCardWidget = ({
   onSelect,
   changedAnswerCount,
   onItemCountChange,
+  onReply,
 }: IWidget) => {
-  const intl = useIntl();
-  const [data, setData] = useState<IComment[]>([]);
-  const [loading, setLoading] = useState(true);
+  const ref = useRef<ActionType>();
+  const [activeKey, setActiveKey] = useState<React.Key | undefined>("active");
+  const [activeNumber, setActiveNumber] = useState<number>(0);
+  const [closeNumber, setCloseNumber] = useState<number>(0);
+  const [count, setCount] = useState<number>(0);
 
   useEffect(() => {
     console.log("changedAnswerCount", changedAnswerCount);
-    const newData = [...data].map((item) => {
-      const newItem = item;
-      if (newItem.id && changedAnswerCount?.id === newItem.id) {
-        newItem.childrenCount = changedAnswerCount.count;
-      }
-      return newItem;
-    });
-    setData(newData);
+    ref.current?.reload();
   }, [changedAnswerCount]);
 
-  useEffect(() => {
-    let url: string = "";
-    if (typeof topicId !== "undefined") {
-      url = `/v2/discussion?view=question-by-topic&id=${topicId}`;
-    } else if (typeof resId !== "undefined") {
-      url = `/v2/discussion?view=question&id=${resId}`;
-    }
-    if (url === "") {
-      return;
-    }
-    setLoading(true);
-
-    get<ICommentListResponse>(url)
-      .then((json) => {
-        console.log(json);
-        if (json.ok) {
-          console.log(intl.formatMessage({ id: "flashes.success" }));
-          const discussions: IComment[] = json.data.rows.map((item) => {
-            return {
-              id: item.id,
-              resId: item.res_id,
-              resType: item.res_type,
-              user: item.editor,
-              title: item.title,
-              parent: item.parent,
-              content: item.content,
-              childrenCount: item.children_count,
-              createdAt: item.created_at,
-              updatedAt: item.updated_at,
-            };
-          });
-          setData(discussions);
-        } else {
-          message.error(json.message);
-        }
-      })
-      .finally(() => {
-        setLoading(false);
-      })
-      .catch((e) => {
-        message.error(e.message);
-      });
-  }, [intl, resId, topicId]);
-
   if (typeof resId === "undefined" && typeof topicId === "undefined") {
     return (
       <Typography.Paragraph>
@@ -95,46 +56,168 @@ const DiscussionListCardWidget = ({
   }
 
   return (
-    <Card title="讨论" extra={"More"}>
-      {loading ? (
-        <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
-      ) : (
-        <DiscussionList
-          data={data}
-          onSelect={(
-            e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
-            comment: IComment
-          ) => {
-            if (typeof onSelect !== "undefined") {
-              onSelect(e, comment);
-            }
-          }}
-          onDelete={(id: string) => {
-            setData((origin) => {
-              return origin.filter((value) => value.id !== id);
-            });
-            if (typeof onItemCountChange !== "undefined") {
-              onItemCountChange(data.length - 1);
-            }
-          }}
-        />
-      )}
+    <>
+      <Collapse bordered={false} defaultActiveKey="list">
+        <Panel header="讨论列表" key="list">
+          <ProList<IComment>
+            itemLayout="vertical"
+            rowKey="id"
+            actionRef={ref}
+            metas={{
+              avatar: {
+                render(dom, entity, index, action, schema) {
+                  return <></>;
+                },
+              },
+              title: {
+                render(dom, entity, index, action, schema) {
+                  return <></>;
+                },
+              },
+              content: {
+                render: (text, row, index, action) => {
+                  return (
+                    <DiscussionItem
+                      data={row}
+                      onSelect={(
+                        e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+                        data: IComment
+                      ) => {
+                        if (typeof onSelect !== "undefined") {
+                          onSelect(e, data);
+                        }
+                      }}
+                      onDelete={() => {
+                        ref.current?.reload();
+                      }}
+                      onReply={() => {
+                        if (typeof onReply !== "undefined") {
+                          onReply(row);
+                        }
+                      }}
+                      onClose={(value: boolean) => {
+                        console.log("comment", row);
+                        put<ICommentRequest, ICommentResponse>(
+                          `/v2/discussion/${row.id}`,
+                          {
+                            title: row.title,
+                            content: row.content,
+                            status: value ? "close" : "active",
+                          }
+                        ).then((json) => {
+                          console.log(json);
+                          if (json.ok) {
+                            ref.current?.reload();
+                          }
+                        });
+                      }}
+                    />
+                  );
+                },
+              },
+            }}
+            request={async (params = {}, sorter, filter) => {
+              let url: string = "/v2/discussion?";
+              if (typeof topicId !== "undefined") {
+                url += `view=question-by-topic&id=${topicId}`;
+              } else if (typeof resId !== "undefined") {
+                url += `view=question&id=${resId}`;
+              } else {
+                return {
+                  total: 0,
+                  succcess: false,
+                };
+              }
+              const offset =
+                ((params.current ? params.current : 1) - 1) *
+                (params.pageSize ? params.pageSize : 20);
+              url += `&limit=${params.pageSize}&offset=${offset}`;
+              url += params.keyword ? "&search=" + params.keyword : "";
+              url += activeKey ? "&status=" + activeKey : "";
+              console.log("url", url);
+              const res = await get<ICommentListResponse>(url);
+              setCount(res.data.active);
+              const items: IComment[] = res.data.rows.map((item, id) => {
+                return {
+                  id: item.id,
+                  resId: item.res_id,
+                  resType: item.res_type,
+                  user: item.editor,
+                  title: item.title,
+                  parent: item.parent,
+                  content: item.content,
+                  status: item.status,
+                  childrenCount: item.children_count,
+                  createdAt: item.created_at,
+                  updatedAt: item.updated_at,
+                };
+              });
+              setActiveNumber(res.data.active);
+              setCloseNumber(res.data.close);
+              return {
+                total: res.data.count,
+                succcess: true,
+                data: items,
+              };
+            }}
+            bordered
+            pagination={{
+              showQuickJumper: true,
+              showSizeChanger: true,
+              pageSize: 20,
+            }}
+            search={false}
+            options={{
+              search: false,
+            }}
+            toolbar={{
+              menu: {
+                activeKey,
+                items: [
+                  {
+                    key: "active",
+                    label: (
+                      <span>
+                        active
+                        {renderBadge(activeNumber, activeKey === "active")}
+                      </span>
+                    ),
+                  },
+                  {
+                    key: "close",
+                    label: (
+                      <span>
+                        close
+                        {renderBadge(closeNumber, activeKey === "close")}
+                      </span>
+                    ),
+                  },
+                ],
+                onChange(key) {
+                  console.log("show course", key);
+                  setActiveKey(key);
+                  ref.current?.reload();
+                },
+              },
+            }}
+          />
+        </Panel>
+      </Collapse>
 
       {resId && resType ? (
-        <CommentCreate
+        <DiscussionCreate
           contentType="markdown"
           resId={resId}
           resType={resType}
           onCreated={(e: IComment) => {
-            const newData = JSON.parse(JSON.stringify(e));
             if (typeof onItemCountChange !== "undefined") {
-              onItemCountChange(data.length + 1);
+              onItemCountChange(count + 1);
             }
-            setData([...data, newData]);
+            ref.current?.reload();
           }}
         />
       ) : undefined}
-    </Card>
+    </>
   );
 };
 

+ 81 - 20
dashboard/src/components/discussion/DiscussionShow.tsx

@@ -16,6 +16,7 @@ import {
   CommentOutlined,
   MessageOutlined,
   ExclamationCircleOutlined,
+  CloseOutlined,
 } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 
@@ -24,6 +25,7 @@ import TimeShow from "../general/TimeShow";
 import Marked from "../general/Marked";
 import { delete_ } from "../../request";
 import { IDeleteResponse } from "../api/Article";
+import { fullUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -32,12 +34,16 @@ interface IWidget {
   onEdit?: Function;
   onSelect?: Function;
   onDelete?: Function;
+  onReply?: Function;
+  onClose?: Function;
 }
 const DiscussionShowWidget = ({
   data,
   onEdit,
   onSelect,
   onDelete,
+  onReply,
+  onClose,
 }: IWidget) => {
   const intl = useIntl();
   const showDeleteConfirm = (id: string, title: string) => {
@@ -79,11 +85,38 @@ const DiscussionShowWidget = ({
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);
     switch (e.key) {
+      case "copy-link":
+        let url = `/discussion/topic/`;
+        if (data.parent) {
+          url += `${data.parent}#${data.id}`;
+        } else {
+          url += data.id;
+        }
+        navigator.clipboard.writeText(fullUrl(url)).then(() => {
+          message.success("链接地址已经拷贝到剪贴板");
+        });
+        break;
+      case "reply":
+        if (typeof onReply !== "undefined") {
+          onReply();
+        }
+        break;
       case "edit":
         if (typeof onEdit !== "undefined") {
           onEdit();
         }
         break;
+      case "close":
+        if (typeof onClose !== "undefined") {
+          onClose(true);
+        }
+        break;
+
+      case "reopen":
+        if (typeof onClose !== "undefined") {
+          onClose(false);
+        }
+        break;
       case "delete":
         if (data.id) {
           showDeleteConfirm(data.id, data.title ? data.title : "");
@@ -97,12 +130,16 @@ const DiscussionShowWidget = ({
   const items: MenuProps["items"] = [
     {
       key: "copy-link",
-      label: "复制链接",
+      label: intl.formatMessage({
+        id: "buttons.copy.link",
+      }),
       icon: <LinkOutlined />,
     },
     {
       key: "reply",
-      label: "回复",
+      label: intl.formatMessage({
+        id: "buttons.reply",
+      }),
       icon: <CommentOutlined />,
       disabled: data.parent ? true : false,
     },
@@ -111,12 +148,32 @@ const DiscussionShowWidget = ({
     },
     {
       key: "edit",
-      label: "编辑",
+      label: intl.formatMessage({
+        id: "buttons.edit",
+      }),
       icon: <EditOutlined />,
     },
+    {
+      key: "close",
+      label: intl.formatMessage({
+        id: "buttons.close",
+      }),
+      icon: <CloseOutlined />,
+      disabled: data.status === "close",
+    },
+    {
+      key: "reopen",
+      label: intl.formatMessage({
+        id: "buttons.open",
+      }),
+      icon: <CloseOutlined />,
+      disabled: data.status === "active",
+    },
     {
       key: "delete",
-      label: "删除",
+      label: intl.formatMessage({
+        id: "buttons.delete",
+      }),
       icon: <DeleteOutlined />,
       danger: true,
       disabled: data.childrenCount ? true : false,
@@ -134,28 +191,28 @@ const DiscussionShowWidget = ({
       size="small"
       title={
         <Space direction="vertical">
-          <Text
-            strong
-            onClick={(e) => {
-              if (typeof onSelect !== "undefined") {
-                onSelect(e, data);
-              }
-            }}
-          >
-            {data.title}
-          </Text>
-          <Text type="secondary">
+          <Text type="secondary" style={{ fontSize: "80%" }}>
             <Space>
               {data.user.nickName}
               <TimeShow
                 type="secondary"
-                time={data.updatedAt}
-                title={intl.formatMessage({
-                  id: "labels.updated-at",
-                })}
+                updatedAt={data.updatedAt}
+                createdAt={data.createdAt}
               />
             </Space>
           </Text>
+          {data.title ? (
+            <Text
+              strong
+              onClick={(e) => {
+                if (typeof onSelect !== "undefined") {
+                  onSelect(e);
+                }
+              }}
+            >
+              {data.title}
+            </Text>
+          ) : undefined}
         </Space>
       }
       extra={
@@ -177,7 +234,11 @@ const DiscussionShowWidget = ({
               </>
             ) : undefined}
           </span>
-          <Dropdown menu={{ items, onClick }} placement="bottomRight">
+          <Dropdown
+            menu={{ items, onClick }}
+            placement="bottomRight"
+            trigger={["click"]}
+          >
             <Button
               shape="circle"
               size="small"

+ 3 - 1
dashboard/src/components/discussion/DiscussionTopic.tsx

@@ -6,11 +6,13 @@ import { IComment } from "./DiscussionItem";
 
 interface IWidget {
   topicId?: string;
+  focus?: string;
   onItemCountChange?: Function;
   onTopicReady?: Function;
 }
 const DiscussionTopicWidget = ({
   topicId,
+  focus,
   onTopicReady,
   onItemCountChange,
 }: IWidget) => {
@@ -19,7 +21,6 @@ const DiscussionTopicWidget = ({
       <DiscussionTopicInfo
         topicId={topicId}
         onReady={(value: IComment) => {
-          console.log("on Topic Ready", value);
           if (typeof onTopicReady !== "undefined") {
             onTopicReady(value);
           }
@@ -27,6 +28,7 @@ const DiscussionTopicWidget = ({
       />
       <Divider />
       <DiscussionTopicChildren
+        focus={focus}
         topicId={topicId}
         onItemCountChange={(count: number, e: string) => {
           //把新建回答的消息传出去。

+ 27 - 20
dashboard/src/components/discussion/DiscussionTopicChildren.tsx

@@ -9,16 +9,24 @@ import DiscussionItem, { IComment } from "./DiscussionItem";
 
 interface IWidget {
   topicId?: string;
+  focus?: string;
   onItemCountChange?: Function;
 }
 const DiscussionTopicChildrenWidget = ({
   topicId,
+  focus,
   onItemCountChange,
 }: IWidget) => {
   const intl = useIntl();
   const [data, setData] = useState<IComment[]>([]);
   const [loading, setLoading] = useState(true);
-
+  useEffect(() => {
+    if (loading === false) {
+      const ele = document.getElementById(`answer-${focus}`);
+      ele?.scrollIntoView();
+      console.log("after render");
+    }
+  });
   useEffect(() => {
     if (typeof topicId === "undefined") {
       return;
@@ -27,9 +35,7 @@ const DiscussionTopicChildrenWidget = ({
 
     get<ICommentListResponse>(`/v2/discussion?view=answer&id=${topicId}`)
       .then((json) => {
-        console.log(json);
         if (json.ok) {
-          console.log("ok", json.data);
           const discussions: IComment[] = json.data.rows.map((item) => {
             return {
               id: item.id,
@@ -69,29 +75,30 @@ const DiscussionTopicChildrenWidget = ({
           }}
           itemLayout="horizontal"
           dataSource={data}
-          renderItem={(item) => (
-            <List.Item>
-              <DiscussionItem
-                data={item}
-                onDelete={() => {
-                  console.log("delete", item.id, data);
-                  if (typeof onItemCountChange !== "undefined") {
-                    onItemCountChange(data.length - 1, item.parent);
-                  }
-                  setData((origin) => {
-                    return origin.filter((value) => value.id !== item.id);
-                  });
-                }}
-              />
-            </List.Item>
-          )}
+          renderItem={(item) => {
+            return (
+              <List.Item>
+                <DiscussionItem
+                  data={item}
+                  isFocus={item.id === focus ? true : false}
+                  onDelete={() => {
+                    if (typeof onItemCountChange !== "undefined") {
+                      onItemCountChange(data.length - 1, item.parent);
+                    }
+                    setData((origin) => {
+                      return origin.filter((value) => value.id !== item.id);
+                    });
+                  }}
+                />
+              </List.Item>
+            );
+          }}
         />
       )}
       <DiscussionCreate
         contentType="markdown"
         parent={topicId}
         onCreated={(e: IComment) => {
-          console.log("create", e);
           const newData = JSON.parse(JSON.stringify(e));
           setData([...data, newData]);
           if (typeof onItemCountChange !== "undefined") {

+ 13 - 11
dashboard/src/components/discussion/DiscussionTopicInfo.tsx

@@ -2,6 +2,7 @@ import { Typography, Space, message } from "antd";
 import { useEffect, useState } from "react";
 import { get } from "../../request";
 import { ICommentResponse } from "../api/Comment";
+import Marked from "../general/Marked";
 import TimeShow from "../general/TimeShow";
 
 import { IComment } from "./DiscussionItem";
@@ -18,9 +19,10 @@ const DiscussionTopicInfoWidget = ({ topicId, onReady }: IWidget) => {
     if (typeof topicId === "undefined") {
       return;
     }
-    get<ICommentResponse>(`/v2/discussion/${topicId}`)
+    const url = `/v2/discussion/${topicId}`;
+    console.log("url", url);
+    get<ICommentResponse>(url)
       .then((json) => {
-        console.log(json);
         if (json.ok) {
           console.log("flashes.success");
           const item = json.data;
@@ -48,25 +50,25 @@ const DiscussionTopicInfoWidget = ({ topicId, onReady }: IWidget) => {
         message.error(e.message);
       });
   }, [topicId]);
+
   return (
     <div>
       <Title editable level={5} style={{ margin: 0 }}>
         {data?.title}
       </Title>
-      <div>
+      <Space direction="vertical">
         <Text type="secondary">
           <Space>
             {data?.user.nickName}
-            <TimeShow time={data?.createdAt} title="创建" />
+            <TimeShow
+              type="secondary"
+              updatedAt={data?.updatedAt}
+              createdAt={data?.createdAt}
+            />
           </Space>
         </Text>
-      </div>
-      <div
-        style={{ maxWidth: 800, overflow: "auto" }}
-        dangerouslySetInnerHTML={{
-          __html: data?.content ? data?.content : "",
-        }}
-      />
+        <Marked text={data?.content} />
+      </Space>
     </div>
   );
 };

+ 5942 - 0
dashboard/src/components/general/PaliEnding.ts

@@ -0,0 +1,5942 @@
+export const getPaliBase = (word: string): string[] => {
+  let parent = new Map<string, number>();
+  paliEnding.forEach((value) => {
+    if (value.type !== ".v." && !value.grammar.includes(".voc.")) {
+      let sEnd2 = word.slice(0 - value.end2.length);
+      if (sEnd2 === value.end2) {
+        const wordParent = word.slice(0, 0 - value.end2.length) + value.end1;
+        parent.set(wordParent, wordParent.length);
+      }
+    }
+  });
+  let output: string[] = [];
+  parent.forEach((value, key, map) => {
+    output.push(key);
+  });
+  output.sort((a, b) => a.length - b.length);
+  return output;
+};
+export const paliEnding = [
+  {
+    end1: "a",
+    end2: "o",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "a",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "aṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ena",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "asmā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ato",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "asmiṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āse",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "esu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "aṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "a",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "aṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ena",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "asmā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ato",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "asmiṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhi",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "āni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "esu",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "enā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "esū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "enā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "amhī",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ānī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ehī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "ebhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "a",
+    end2: "esū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "e",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "aṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ato",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ānaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ābhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ābhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āsu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ābhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "ābhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ā",
+    end2: "āsū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "issa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "issa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ismā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ismiṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ayo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ayo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ayo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "assa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ismā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ismiṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhi",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "yaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ihi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ibhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ihi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ibhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "isu",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "issā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "issā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "assā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "imhī",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ihī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ibhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ihī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "ibhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "i",
+    end2: "isū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "issa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "issa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ismā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "imhā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ismiṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "imhi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ayaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ini",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ito",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "i",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īyanaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īyanaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyanaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "iyanaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "isu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "issā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "issā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "imhī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "ībhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "īsū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ī",
+    end2: "isū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "usmā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "usmiṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ave",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "usmā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "usmiṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhi",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūni",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsu",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "u",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uto",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūvo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "umhī",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūnī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsū",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "u",
+    end2: "ūsū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ussa",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uno",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "unā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "usmā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "umhā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "usmiṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "umhi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ave",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "avo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūsu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uto",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "uyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūnaṃ",
+    type: ".n.",
+    grammar: ".f.$.pl.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhi",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūsu",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ussā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "umhī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūsū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.inst.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūbhī",
+    type: ".n.",
+    grammar: ".f.$.pl.$.abl.",
+    confidence: "100",
+  },
+  {
+    end1: "ū",
+    end2: "ūsū",
+    type: ".n.",
+    grammar: ".f.$.pl.$.loc.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "etha",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eraṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issanti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issate",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issante",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issare",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "e",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "āmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issāmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyāsi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyātha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "etho",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "eyyavho",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "issavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "etha",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eraṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issanti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issate",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issante",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issare",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "e",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "āmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issāmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyāsi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyātha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "etho",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "eyyavho",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "āti",
+    end2: "issavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "etha",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eraṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issanti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issate",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issante",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issare",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "e",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "āmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issāmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyāsi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyātha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "etho",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "eyyavho",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "eti",
+    end2: "issavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "etha",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eraṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issanti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issate",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issante",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issare",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "e",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "āmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issāmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyāsi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyātha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "etho",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "eyyavho",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "oti",
+    end2: "issavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ti",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "nti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "te",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "nte",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "re",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssanti",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssate",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssante",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssare",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "tu",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ntu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "taṃ",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ntaṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "sā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssa",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssati",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssaṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssatha",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssiṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "e",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mahe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mhase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssāmi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssāma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssāmhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssāmase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "mi",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ma",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssa",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssamhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssaṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssāmhase",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "si",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "tha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "vhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.pres.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.fut.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "hi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ta",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssu",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "vho",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.imp.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssa",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssasi",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssatha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssase",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "ssavhe",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.cond.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "eyya",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ti",
+    end2: "eyyuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.opt.",
+    confidence: "100",
+  },
+  {
+    end1: "ati",
+    end2: "i",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ī",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "iṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "uṃū",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "a",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "tthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "atthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "iṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "aṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "imha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "imhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "imhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "i",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "o",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "ttha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ati",
+    end2: "vhaṃ",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ī",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "iṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "uṃū",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "tthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "atthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "iṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "aṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "imha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "imhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "imhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "o",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "ttha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "āti",
+    end2: "vhaṃ",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ī",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "iṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "uṃū",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "tthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "atthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "iṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "aṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "imha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "imhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "imhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "o",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "ttha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "eti",
+    end2: "vhaṃ",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ī",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "iṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "uṃū",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "tthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "atthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "iṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "aṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "imha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "imhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "a",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "imhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "i",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "o",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ā",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "ttha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "se",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "oti",
+    end2: "vhaṃ",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "si",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sī",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "siṃsu",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "suṃū",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sā",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sa",
+    type: ".v.",
+    grammar: ".3p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "stthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "satthuṃ",
+    type: ".v.",
+    grammar: ".3p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "siṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "saṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sṃ",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sa",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sā",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "simha",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "simhā",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sa",
+    type: ".v.",
+    grammar: ".1p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "simhe",
+    type: ".v.",
+    grammar: ".1p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "si",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "so",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sā",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sttha",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "sse",
+    type: ".v.",
+    grammar: ".2p.$.sg.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "ti",
+    end2: "svhaṃ",
+    type: ".v.",
+    grammar: ".2p.$.pl.$.aor.",
+    confidence: "90",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asi",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "o",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "āse",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "o",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ān",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ato",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asi",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "aṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "āya",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "o",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "ato",
+    type: ".n.",
+    grammar: ".nt.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "a",
+    end2: "asī",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "ā",
+    end2: "ā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "ā",
+    end2: "āto",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "ā",
+    end2: "a",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "ā",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ito",
+    type: ".n.",
+    grammar: ".m.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ini",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "o",
+    type: ".n.",
+    grammar: ".m.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ino",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ihi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ibhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ihi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ibhi",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "inaṃ",
+    type: ".n.",
+    grammar: ".m.$.pl.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "isu",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "i",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ito",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ini",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "e",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "o",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "iṃ",
+    type: ".n.",
+    grammar: ".nt.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.sg.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ito",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "myā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "o",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "āyaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "u",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ī",
+    type: ".n.",
+    grammar: ".f.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ihī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ibhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ihī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "ibhī",
+    type: ".n.",
+    grammar: ".m.$.pl.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "i",
+    end2: "isū",
+    type: ".n.",
+    grammar: ".m.$.pl.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "inī",
+    type: ".n.",
+    grammar: ".m.$.sg.$.voc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.gen.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.dat.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.inst.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yā",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yaṃ",
+    type: ".n.",
+    grammar: ".f.$.sg.$.loc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "yo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "i",
+    type: ".n.",
+    grammar: ".m.$.sg.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "iyo",
+    type: ".n.",
+    grammar: ".m.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "iye",
+    type: ".n.",
+    grammar: ".m.$.pl.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "iyaṃ",
+    type: ".n.",
+    grammar: ".m.$.sg.$.acc.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "ito",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "īto",
+    type: ".n.",
+    grammar: ".f.$.sg.$.abl.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "50",
+  },
+  {
+    end1: "ī",
+    end2: "āyo",
+    type: ".n.",
+    grammar: ".f.$.pl.$.nom.",
+    confidence: "50",
+  },
+];

+ 15 - 0
dashboard/src/components/general/ParserError.tsx

@@ -0,0 +1,15 @@
+import { Popover } from "antd";
+import { WarningOutlined } from "@ant-design/icons";
+
+interface IWidget {
+  children?: React.ReactNode;
+}
+const ParserErrorWidget = ({ children }: IWidget) => {
+  return (
+    <Popover content={children} placement="bottom">
+      <WarningOutlined style={{ color: "red" }} />
+    </Popover>
+  );
+};
+
+export default ParserErrorWidget;

+ 108 - 31
dashboard/src/components/general/TermTextAreaMenu.tsx

@@ -1,4 +1,21 @@
-import { useEffect } from "react";
+import { Space, Typography } from "antd";
+import { useEffect, useState } from "react";
+import { RobotOutlined } from "@ant-design/icons";
+
+import { TermIcon } from "../../assets/icon";
+import { useAppSelector } from "../../hooks";
+import { getTerm } from "../../reducers/term-vocabulary";
+import { PaliToEn } from "../../utils";
+import { getPaliBase } from "./PaliEnding";
+
+const { Text } = Typography;
+
+interface IWordWithEn {
+  word: string;
+  en: string;
+  isBase?: boolean;
+  isTerm?: boolean;
+}
 
 interface IWidget {
   items?: string[];
@@ -10,54 +27,114 @@ interface IWidget {
   onSelect?: Function;
 }
 const TermTextAreaMenuWidget = ({
-  items,
-  searchKey,
+  items = [],
+  searchKey = "",
   maxItem = 10,
   visible = false,
   currIndex = 0,
   onChange,
   onSelect,
 }: IWidget) => {
-  console.log("currIndex", currIndex);
-  const filteredItem = searchKey
-    ? items?.filter((value) => value.slice(0, searchKey.length) === searchKey)
-    : items;
+  const [filtered, setFiltered] = useState<IWordWithEn[]>();
+  const [wordList, setWordList] = useState<IWordWithEn[]>();
+  const sysTerms = useAppSelector(getTerm);
+  console.log("items", items);
+  useEffect(() => {
+    //本句单词
+    const mWords = items?.map((item) => {
+      return {
+        word: item,
+        en: PaliToEn(item),
+      };
+    });
+
+    //计算这些单词的base
+    let parents: string[] = [];
+    items?.forEach((value) => {
+      getPaliBase(value).forEach((base) => {
+        if (!parents.includes(base) && !items.includes(base)) {
+          parents.push(base);
+        }
+      });
+    });
+
+    const term = sysTerms ? sysTerms?.map((item) => item.word) : [];
+    //本句单词parent
+    const parentTerm = parents?.map((item) => {
+      const inSystem = term.includes(item);
+      return {
+        word: item,
+        en: PaliToEn(item),
+        isBase: !inSystem,
+        isTerm: inSystem,
+      };
+    });
+
+    //社区术语
+    const sysTerm = term
+      .filter((value) => !parents.includes(value))
+      .map((item) => {
+        return {
+          word: item,
+          en: PaliToEn(item),
+          isTerm: true,
+        };
+      });
+    setWordList([...parentTerm, ...mWords, ...sysTerm]);
+
+    //此处千万不能加其他dependency 否则会引起无限循环
+  }, [items]);
+
+  useEffect(() => {
+    const filteredItems =
+      searchKey !== ""
+        ? wordList?.filter(
+            (value) => value.en.slice(0, searchKey.length) === searchKey
+          )
+        : wordList;
+    setFiltered(filteredItems);
+  }, [wordList, searchKey]);
+
   useEffect(() => {
-    if (filteredItem && typeof onChange !== "undefined") {
-      if (currIndex < filteredItem?.length) {
-        onChange(filteredItem[currIndex]);
+    if (filtered && filtered.length > 0 && typeof onChange !== "undefined") {
+      if (currIndex < filtered.length) {
+        onChange(filtered[currIndex].word);
       } else {
-        onChange(filteredItem[filteredItem.length - 1]);
+        onChange(filtered[filtered.length - 1].word);
       }
     }
-  }, [currIndex]);
+  }, [currIndex, filtered]);
 
   if (visible) {
     return (
       <>
         <div className="term_at_menu_input" key="head">
-          {searchKey}
-          {"|"}
+          {`${searchKey}|`}
         </div>
         <ul className="term_at_menu_ul">
-          {filteredItem?.map((item, index) => {
-            if (index < maxItem) {
-              return (
-                <li
-                  key={index}
-                  className={index === currIndex ? "term_focus" : undefined}
-                  onClick={() => {
-                    if (typeof onSelect !== "undefined") {
-                      onSelect(item);
-                    }
-                  }}
+          {filtered?.slice(0, maxItem).map((item, index) => {
+            return (
+              <li
+                key={index}
+                className={index === currIndex ? "term_focus" : undefined}
+                onClick={() => {
+                  if (typeof onSelect !== "undefined") {
+                    onSelect(item.word);
+                  }
+                }}
+              >
+                <Space
+                  style={{ width: "100%", justifyContent: "space-between" }}
                 >
-                  {item}
-                </li>
-              );
-            } else {
-              return undefined;
-            }
+                  <Text strong={item.isBase || item.isTerm}>{item.word}</Text>
+                  {item.isTerm ? (
+                    <TermIcon />
+                  ) : item.isBase ? (
+                    <RobotOutlined />
+                  ) : undefined}
+                </Space>
+              </li>
+            );
           })}
         </ul>
       </>

+ 50 - 14
dashboard/src/components/general/TimeShow.tsx

@@ -8,7 +8,9 @@ const { Text } = Typography;
 interface IWidgetTimeShow {
   showIcon?: boolean;
   showTooltip?: boolean;
-  time?: string;
+  showLabel?: boolean;
+  createdAt?: string;
+  updatedAt?: string;
   title?: string;
   type?: BaseType;
 }
@@ -16,7 +18,9 @@ interface IWidgetTimeShow {
 const TimeShowWidget = ({
   showIcon = true,
   showTooltip = true,
-  time,
+  showLabel = true,
+  createdAt,
+  updatedAt,
   title,
   type,
 }: IWidgetTimeShow) => {
@@ -24,8 +28,41 @@ const TimeShowWidget = ({
   const [passTime, setPassTime] = useState<string>();
   const [mTime, setMTime] = useState(0);
 
+  let mTitle: string | undefined;
+  let showTime: string | undefined;
+  if (typeof title === "undefined") {
+    if (updatedAt && createdAt) {
+      if (updatedAt === createdAt) {
+        mTitle = intl.formatMessage({
+          id: "labels.created-at",
+        });
+        showTime = createdAt;
+      } else {
+        mTitle = intl.formatMessage({
+          id: "labels.updated-at",
+        });
+        showTime = updatedAt;
+      }
+    } else if (createdAt) {
+      mTitle = intl.formatMessage({
+        id: "labels.created-at",
+      });
+      showTime = createdAt;
+    } else if (updatedAt) {
+      mTitle = intl.formatMessage({
+        id: "labels.updated-at",
+      });
+      showTime = updatedAt;
+    } else {
+      mTitle = undefined;
+      showTime = "";
+    }
+  } else {
+    mTitle = title;
+  }
+
   useEffect(() => {
-    if (typeof time === "undefined") {
+    if (typeof createdAt === "undefined" && typeof updatedAt === "undefined") {
       return;
     }
     let timer = setInterval(() => {
@@ -37,28 +74,27 @@ const TimeShowWidget = ({
   }, []);
 
   useEffect(() => {
-    if (typeof time !== "undefined" && time !== "") {
-      setPassTime(getPassDataTime(time));
+    if (typeof showTime !== "undefined" && showTime !== "") {
+      setPassTime(getPassDataTime(showTime));
     }
-  }, [mTime, time]);
+  }, [mTime, showTime]);
 
-  if (typeof time === "undefined" || time === "") {
+  if (typeof showTime === "undefined") {
     return <></>;
   }
+
   const icon = showIcon ? <FieldTimeOutlined /> : <></>;
 
-  const tooltip: string = getFullDataTime(time);
+  const tooltip: string = getFullDataTime(showTime);
   const color = "lime";
   function getPassDataTime(t: string): string {
     let currDate = new Date();
     const time = new Date(t);
     let pass = currDate.getTime() - time.getTime();
     let strPassTime = "";
-    if (pass < 60 * 1000) {
-      //二分钟内
-      strPassTime =
-        Math.floor(pass / 1000) +
-        intl.formatMessage({ id: "utilities.time.secs_ago" });
+    if (pass < 100 * 1000) {
+      //一分钟内
+      strPassTime = intl.formatMessage({ id: "utilities.time.now" });
     } else if (pass < 3600 * 1000) {
       //二小时内
       strPassTime =
@@ -106,7 +142,7 @@ const TimeShowWidget = ({
       <Text type={type}>
         <Space>
           {icon}
-          {title}
+          {mTitle}
           {passTime}
         </Space>
       </Text>

+ 0 - 1
dashboard/src/components/group/AddMember.tsx

@@ -22,7 +22,6 @@ const AddMemberWidget = ({ groupId, onCreated }: IWidget) => {
   const form = (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
-        // TODO
         console.log(values);
         if (typeof groupId !== "undefined") {
           post<IGroupMemberData, IGroupMemberResponse>("/v2/group-member", {

+ 0 - 1
dashboard/src/components/group/GroupCreate.tsx

@@ -25,7 +25,6 @@ const GroupCreateWidget = ({ studio, onCreate }: IWidgetGroupCreate) => {
     <ProForm<IFormData>
       formRef={formRef}
       onFinish={async (values: IFormData) => {
-        // TODO
         if (typeof studio === "undefined") {
           return;
         }

+ 0 - 1
dashboard/src/components/group/GroupFile.tsx

@@ -102,7 +102,6 @@ const GroupFileWidget = ({ groupId }: IWidget) => {
           },
         }}
         request={async (params = {}, sorter, filter) => {
-          // TODO
           console.log(params, sorter, filter);
 
           let url = `/v2/share?view=group&id=${groupId}`;

+ 0 - 1
dashboard/src/components/group/GroupMember.tsx

@@ -56,7 +56,6 @@ const GroupMemberWidget = ({ groupId }: IWidgetGroupFile) => {
         }}
         showActions="hover"
         request={async (params = {}, sorter, filter) => {
-          // TODO
           console.log(params, sorter, filter);
 
           let url = `/v2/group-member?view=group&id=${groupId}`;

+ 0 - 1
dashboard/src/components/invite/InviteCreate.tsx

@@ -37,7 +37,6 @@ const InviteCreateWidget = ({ studio, onCreate }: IWidget) => {
     <ProForm<IFormData>
       formRef={formRef}
       onFinish={async (values: IFormData) => {
-        // TODO
         if (typeof studio === "undefined") {
           return;
         }

+ 1 - 1
dashboard/src/components/library/FooterBar.tsx

@@ -6,7 +6,7 @@ const { Paragraph } = Typography;
 
 const FooterBarWidget = () => {
   //Library foot bar
-  // TODO
+  // TODO 补充项目信息
   return (
     <Footer>
       <Row>

+ 0 - 2
dashboard/src/components/library/HeadBar.tsx

@@ -122,8 +122,6 @@ type IWidgetHeadBar = {
 };
 const HeadBarWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
   //Library head bar
-  // TODO
-
   return (
     <Header
       className="header"

+ 0 - 1
dashboard/src/components/share/Collaborator.tsx

@@ -61,7 +61,6 @@ const CollaboratorWidget = ({ resId, load = false, onReload }: IWidget) => {
       }
       showActions="hover"
       request={async (params = {}, sorter, filter) => {
-        // TODO
         console.log(params, sorter, filter);
 
         let url = `/v2/share?view=res&id=${resId}`;

+ 0 - 2
dashboard/src/components/share/CollaboratorAdd.tsx

@@ -34,8 +34,6 @@ const CollaboratorAddWidget = ({ resId, resType, onSuccess }: IWidget) => {
     <ProForm<IFormData>
       formRef={formRef}
       onFinish={async (values: IFormData) => {
-        // TODO
-
         if (typeof resId !== "undefined") {
           const postData: IShareRequest = {
             user_id:

+ 1 - 1
dashboard/src/components/template/Article.tsx

@@ -13,7 +13,7 @@ interface IWidgetChapterCtl {
   style?: TDisplayStyle;
 }
 
-const ArticleCtl = ({
+export const ArticleCtl = ({
   type,
   id,
   channel,

+ 246 - 0
dashboard/src/components/template/Builder/ArticleTpl.tsx

@@ -0,0 +1,246 @@
+import { useEffect, useState } from "react";
+import {
+  Button,
+  Divider,
+  Input,
+  Modal,
+  Select,
+  Space,
+  Tabs,
+  Typography,
+} from "antd";
+import { FolderOpenOutlined } from "@ant-design/icons";
+
+import { ArticleCtl, TDisplayStyle } from "../Article";
+import { ArticleType } from "../../article/Article";
+import { useIntl } from "react-intl";
+import ArticleListModal from "../../article/ArticleListModal";
+import { useAppSelector } from "../../../hooks";
+import { currentUser } from "../../../reducers/current-user";
+import ChannelTableModal from "../../channel/ChannelTableModal";
+import { IChannel } from "../../channel/Channel";
+const { TextArea } = Input;
+const { Paragraph } = Typography;
+
+interface IWidget {
+  type?: ArticleType;
+  id?: string;
+  title?: string;
+  style?: TDisplayStyle;
+  channel?: string;
+  onSelect?: Function;
+  onCancel?: Function;
+}
+const ArticleTplWidget = ({
+  type,
+  id,
+  channel,
+  title,
+  style = "modal",
+}: IWidget) => {
+  const intl = useIntl(); //i18n
+  const [currTitle, setCurrTitle] = useState(title);
+  const [currChannel, setCurrChannel] = useState(channel);
+  const [styleText, setStyleText] = useState(style);
+  const [typeText, setTypeText] = useState(type);
+  const [idText, setIdText] = useState(id);
+  const [tplText, setTplText] = useState("");
+  const user = useAppSelector(currentUser);
+
+  const ids = id?.split("_");
+  const id1 = ids ? ids[0] : undefined;
+  const channels = ids
+    ? ids.length > 1
+      ? ids?.slice(1)
+      : undefined
+    : undefined;
+
+  useEffect(() => {
+    setCurrTitle(title);
+  }, [title]);
+  useEffect(() => {
+    let tplText = `{{article|\n`;
+    tplText += `type=${typeText}|\n`;
+    tplText += `id=${idText}|\n`;
+    tplText += `title=${currTitle}|\n`;
+    tplText += currChannel ? `channel=${currChannel}|\n` : "";
+    tplText += `style=${styleText}`;
+
+    tplText += channels ? `channel=${channels}` : "";
+    tplText += "}}";
+
+    setTplText(tplText);
+  }, [currTitle, styleText, type, id1, channels, typeText, idText]);
+  return (
+    <>
+      <Space direction="vertical" style={{ width: 500 }}>
+        <Space style={{ width: 500 }}>
+          {"标题:"}
+          <Input
+            width={400}
+            value={currTitle}
+            placeholder="Title"
+            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+              setCurrTitle(event.target.value);
+            }}
+          />
+        </Space>
+        <Space>
+          {"类别:"}
+          <Select
+            disabled={type ? true : false}
+            defaultValue={type}
+            style={{ width: 120 }}
+            onChange={(value: string) => {
+              console.log(`selected ${value}`);
+              setTypeText(value as ArticleType);
+            }}
+            options={["article", "chapter", "para"].map((item) => {
+              return { value: item, label: item };
+            })}
+          />
+        </Space>
+        <Space style={{ width: 500 }}>
+          {"id:"}
+          <Space>
+            <Input
+              disabled={id ? true : false}
+              defaultValue={id}
+              width={400}
+              value={idText}
+              placeholder="Id"
+              onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                setIdText(event.target.value);
+              }}
+            />
+            {typeText === "article" ? (
+              <ArticleListModal
+                studioName={user?.realName}
+                trigger={<Button icon={<FolderOpenOutlined />} type="text" />}
+                multiple={false}
+                onSelect={(id: string, title: string) => {
+                  setIdText(id);
+                  setCurrTitle(title);
+                }}
+              />
+            ) : undefined}
+          </Space>
+        </Space>
+        <Space style={{ width: 500 }}>
+          {"channel:"}
+          <Space>
+            <Input
+              defaultValue={channel}
+              width={400}
+              value={currChannel}
+              placeholder="channel"
+              onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                setCurrChannel(event.target.value);
+              }}
+            />
+            <ChannelTableModal
+              trigger={<Button icon={<FolderOpenOutlined />} type="text" />}
+              onSelect={(channel: IChannel) => {
+                setCurrChannel(channel.id);
+              }}
+            />
+          </Space>
+        </Space>
+        <Space>
+          {"显示为:"}
+          <Select
+            defaultValue={style}
+            style={{ width: 120 }}
+            onChange={(value: string) => {
+              console.log(`selected ${value}`);
+              setStyleText(value as TDisplayStyle);
+            }}
+            options={["modal", "card", "toggle"].map((item) => {
+              return { value: item, label: item };
+            })}
+          />
+        </Space>
+        <Tabs
+          size="small"
+          defaultActiveKey="preview"
+          items={[
+            {
+              label: intl.formatMessage({
+                id: "buttons.preview",
+              }),
+              key: "preview",
+              children: (
+                <ArticleCtl
+                  type={typeText}
+                  id={idText}
+                  title={currTitle}
+                  style={styleText}
+                />
+              ),
+            },
+            {
+              label: `Code`,
+              key: "code",
+              children: (
+                <TextArea
+                  value={tplText}
+                  rows={4}
+                  placeholder="maxLength is 6"
+                  maxLength={6}
+                />
+              ),
+            },
+          ]}
+        />
+        <Divider></Divider>
+        <Paragraph copyable={{ text: tplText }}>复制到剪贴板</Paragraph>
+      </Space>
+    </>
+  );
+};
+
+interface IModalWidget {
+  type?: ArticleType;
+  id?: string;
+  title?: string;
+  style?: TDisplayStyle;
+  trigger?: JSX.Element;
+}
+export const ArticleTplModal = ({
+  type,
+  id,
+  title,
+  style = "modal",
+  trigger,
+}: IModalWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={"80%"}
+        title="生成模版"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <ArticleTplWidget type={type} id={id} title={title} style={style} />
+      </Modal>
+    </>
+  );
+};
+
+export default ArticleTplWidget;

+ 74 - 0
dashboard/src/components/template/Builder/Builder.tsx

@@ -0,0 +1,74 @@
+import { useState } from "react";
+import { Col, Modal, Row, Tree } from "antd";
+import { Key } from "antd/lib/table/interface";
+import ArticleTpl from "./ArticleTpl";
+
+interface DataNode {
+  title: React.ReactNode;
+  key: string;
+  isLeaf?: boolean;
+  children?: DataNode[];
+}
+
+interface tplListNode {
+  name: string;
+  component?: React.ReactNode;
+}
+
+interface IWidget {
+  trigger?: React.ReactNode;
+}
+const TplBuilderWidget = ({ trigger }: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [template, setTemplate] =
+    useState<React.ReactNode>("在左侧列表选择一个模版");
+
+  const tplList: tplListNode[] = [
+    { name: "article", component: <ArticleTpl /> },
+    { name: "note" },
+    { name: "term" },
+  ];
+  const treeData: DataNode[] = tplList.map((item) => {
+    return { title: item.name, key: item.name };
+  });
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={700}
+        footer={false}
+        title="template builder"
+        open={isModalOpen}
+        onCancel={handleCancel}
+      >
+        <Row>
+          <Col span={8}>
+            <Tree
+              treeData={treeData}
+              onSelect={(selectedKeys: Key[]) => {
+                if (selectedKeys.length > 0) {
+                  const tpl = tplList.find(
+                    (value) => value.name === selectedKeys[0]
+                  )?.component;
+                  setTemplate(tpl);
+                }
+              }}
+            />
+          </Col>
+          <Col span={16}>{template}</Col>
+        </Row>
+      </Modal>
+    </>
+  );
+};
+
+export default TplBuilderWidget;

+ 7 - 2
dashboard/src/components/template/MdView.tsx

@@ -1,6 +1,6 @@
 import { Typography } from "antd";
 import { TCodeConvertor, XmlToReact } from "./utilities";
-const { Paragraph } = Typography;
+const { Paragraph, Text } = Typography;
 
 interface IWidget {
   html?: string;
@@ -18,7 +18,12 @@ const Widget = ({
   convertor,
   style,
 }: IWidget) => {
-  const jsx = html ? XmlToReact(html, wordWidget, convertor) : placeholder;
+  const jsx =
+    html && html.trim() !== "" ? (
+      XmlToReact(html, wordWidget, convertor)
+    ) : (
+      <Text type="secondary">{placeholder}</Text>
+    );
   return (
     <Paragraph style={style} className={className}>
       {jsx}

+ 0 - 1
dashboard/src/components/template/Nissaya.tsx

@@ -1,4 +1,3 @@
-import { Space } from "antd";
 import NissayaMeaning from "./Nissaya/NissayaMeaning";
 import PaliText from "./Wbw/PaliText";
 

+ 1 - 1
dashboard/src/components/template/ParaHandle.tsx

@@ -1,4 +1,4 @@
-import { Button, Divider, Dropdown, MenuProps, message, Popover } from "antd";
+import { Button, Divider, Dropdown, MenuProps, message } from "antd";
 import { useNavigate } from "react-router-dom";
 import { fullUrl } from "../../utils";
 

+ 39 - 2
dashboard/src/components/template/SentEdit.tsx

@@ -9,6 +9,16 @@ import { ITocPathNode } from "../corpus/TocPath";
 import SentContent from "./SentEdit/SentContent";
 import SentTab from "./SentEdit/SentTab";
 import { IWbw } from "./Wbw/WbwWord";
+import { ArticleMode } from "../article/Article";
+import { TChannelType } from "../api/Channel";
+
+export interface IResNumber {
+  translation?: number;
+  nissaya?: number;
+  commentary?: number;
+  origin?: number;
+  sim?: number;
+}
 
 export interface ISuggestionCount {
   suggestion?: number;
@@ -16,7 +26,7 @@ export interface ISuggestionCount {
 }
 export interface ISentence {
   id?: string;
-  content: string;
+  content: string | null;
   contentType?: TContentType;
   html: string;
   book: number;
@@ -29,6 +39,7 @@ export interface ISentence {
   channel: IChannel;
   studio?: IStudio;
   updateAt: string;
+  createdAt?: string;
   suggestionCount?: ISuggestionCount;
   openInEditMode?: boolean;
   translationChannels?: string[];
@@ -56,6 +67,7 @@ export interface IWidgetSentEditInner {
   originNum: number;
   simNum?: number;
   compact?: boolean;
+  mode?: ArticleMode;
 }
 export const SentEditInner = ({
   id,
@@ -74,17 +86,38 @@ export const SentEditInner = ({
   originNum,
   simNum,
   compact = false,
+  mode,
 }: IWidgetSentEditInner) => {
   const [wbwData, setWbwData] = useState<IWbw[]>();
   const [magicDict, setMagicDict] = useState<string>();
   const [magicDictLoading, setMagicDictLoading] = useState(false);
   const [isCompact, setIsCompact] = useState(compact);
+  const [articleMode, setArticleMode] = useState<ArticleMode | undefined>(mode);
+  const [loadedRes, setLoadedRes] = useState<IResNumber>();
 
+  useEffect(() => {
+    const validRes = (value: ISentence, type: TChannelType) =>
+      value.channel.type === type &&
+      value.content &&
+      value.content.trim().length > 0;
+    if (translation) {
+      const res = {
+        translation: translation.filter((value) =>
+          validRes(value, "translation")
+        ).length,
+        nissaya: translation.filter((value) => validRes(value, "nissaya"))
+          .length,
+        commentary: translation.filter((value) => validRes(value, "commentary"))
+          .length,
+      };
+      setLoadedRes(res);
+    }
+  }, [translation]);
   useEffect(() => {
     const content = origin?.find(
       (value) => value.channel.type === "wbw"
     )?.content;
-    if (typeof content !== "undefined") {
+    if (content) {
       setWbwData(JSON.parse(content));
     }
   }, []);
@@ -108,6 +141,7 @@ export const SentEditInner = ({
         layout={layout}
         magicDict={magicDict}
         compact={isCompact}
+        mode={articleMode}
         onWbwChange={(data: IWbw[]) => {
           setWbwData(data);
         }}
@@ -129,14 +163,17 @@ export const SentEditInner = ({
         commNum={commNum}
         originNum={originNum}
         simNum={simNum}
+        loadedRes={loadedRes}
         wbwData={wbwData}
         magicDictLoading={magicDictLoading}
         compact={isCompact}
+        mode={articleMode}
         onMagicDict={(type: string) => {
           setMagicDict(type);
           setMagicDictLoading(true);
         }}
         onCompact={(value: boolean) => setIsCompact(value)}
+        onModeChange={(value: ArticleMode | undefined) => setArticleMode(value)}
       />
     </Card>
   );

+ 17 - 4
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -19,17 +19,30 @@ const EditInfoWidget = ({ data, isPr = false, compact = false }: IWidget) => {
     <Space>
       <Channel {...data.channel} />
       <User {...data.editor} showAvatar={isPr ? true : false} />
-      <span>edit</span>
       {data.prEditAt ? (
-        <TimeShow time={data.prEditAt} />
+        <TimeShow
+          type="secondary"
+          updatedAt={data.prEditAt}
+          createdAt={data.createdAt}
+        />
       ) : (
-        <TimeShow time={data.updateAt} />
+        <TimeShow
+          type="secondary"
+          updatedAt={data.updateAt}
+          createdAt={data.createdAt}
+        />
       )}
       {data.acceptor ? (
         <User {...data.acceptor} showAvatar={false} />
       ) : undefined}
       {data.acceptor ? "accept at" : undefined}
-      {data.prEditAt ? <TimeShow time={data.updateAt} /> : undefined}
+      {data.prEditAt ? (
+        <TimeShow
+          type="secondary"
+          updatedAt={data.updateAt}
+          showLabel={false}
+        />
+      ) : undefined}
     </Space>
   );
   return (

+ 8 - 1
dashboard/src/components/template/SentEdit/SentAdd.tsx

@@ -4,17 +4,24 @@ import { PlusOutlined } from "@ant-design/icons";
 
 import { IChannel } from "../../channel/Channel";
 import ChannelTableModal from "../../channel/ChannelTableModal";
+import { TChannelType } from "../../api/Channel";
 
 interface IWidget {
   disableChannels?: string[];
+  type?: TChannelType;
   onSelect?: Function;
 }
-const Widget = ({ disableChannels, onSelect }: IWidget) => {
+const Widget = ({
+  disableChannels,
+  type = "translation",
+  onSelect,
+}: IWidget) => {
   const [channelPickerOpen, setChannelPickerOpen] = useState(false);
 
   return (
     <ChannelTableModal
       disableChannels={disableChannels}
+      channelType={type}
       trigger={
         <Button
           type="dashed"

+ 2 - 1
dashboard/src/components/template/SentEdit/SentCanRead.tsx

@@ -103,6 +103,7 @@ const SentCanReadWidget = ({
       </div>
       <SentAdd
         disableChannels={channels}
+        type={type}
         onSelect={(channel: IChannel) => {
           if (typeof user === "undefined") {
             return;
@@ -134,7 +135,7 @@ const SentCanReadWidget = ({
       {sentData.map((item, id) => {
         return (
           <SentCell
-            data={item}
+            initValue={item}
             key={id}
             isPr={false}
             editMode={item.openInEditMode}

+ 86 - 75
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -1,4 +1,6 @@
 import { useEffect, useState } from "react";
+import { useIntl } from "react-intl";
+import { Divider } from "antd";
 
 import { ISentence } from "../SentEdit";
 import SentEditMenu from "./SentEditMenu";
@@ -6,7 +8,6 @@ import SentCellEditable from "./SentCellEditable";
 import MdView from "../MdView";
 import EditInfo from "./EditInfo";
 import SuggestionToolbar from "./SuggestionToolbar";
-import { Divider, Space } from "antd";
 import { useAppSelector } from "../../../hooks";
 import { sentence } from "../../../reducers/accept-pr";
 import { IWbw } from "../Wbw/WbwWord";
@@ -14,17 +15,16 @@ import { my_to_roman } from "../../code/my";
 import SentWbwEdit, { sentSave } from "./SentWbwEdit";
 import { getEnding } from "../../../reducers/nissaya-ending-vocabulary";
 import { nissayaBase } from "../Nissaya/NissayaMeaning";
-import { useIntl } from "react-intl";
 
 interface IWidget {
-  data: ISentence;
+  initValue: ISentence;
   wordWidget?: boolean;
   isPr?: boolean;
   editMode?: boolean;
   compact?: boolean;
 }
 const SentCellWidget = ({
-  data,
+  initValue,
   wordWidget = false,
   isPr = false,
   editMode = false,
@@ -32,27 +32,27 @@ const SentCellWidget = ({
 }: IWidget) => {
   const intl = useIntl();
   const [isEditMode, setIsEditMode] = useState(editMode);
-  const [sentData, setSentData] = useState<ISentence>(data);
+  const [sentData, setSentData] = useState<ISentence>(initValue);
   const endings = useAppSelector(getEnding);
   const acceptPr = useAppSelector(sentence);
   const [prOpen, setPrOpen] = useState(false);
-
+  /*
   useEffect(() => {
     setSentData(data);
   }, [data]);
-
+*/
   useEffect(() => {
     if (typeof acceptPr !== "undefined" && !isPr) {
       if (
-        acceptPr.book === data.book &&
-        acceptPr.para === data.para &&
-        acceptPr.wordStart === data.wordStart &&
-        acceptPr.wordEnd === data.wordEnd &&
-        acceptPr.channel.id === data.channel.id
+        acceptPr.book === initValue.book &&
+        acceptPr.para === initValue.para &&
+        acceptPr.wordStart === initValue.wordStart &&
+        acceptPr.wordEnd === initValue.wordEnd &&
+        acceptPr.channel.id === initValue.channel.id
       )
         setSentData(acceptPr);
     }
-  }, [acceptPr, data, isPr]);
+  }, [acceptPr, initValue, isPr]);
   const sid = `${sentData.book}_${sentData.para}_${sentData.wordStart}_${sentData.wordEnd}_${sentData.channel.id}`;
 
   return (
@@ -65,7 +65,7 @@ const SentCellWidget = ({
         />
       )}
       <SentEditMenu
-        data={data}
+        data={sentData}
         onModeChange={(mode: string) => {
           if (mode === "edit") {
             setIsEditMode(true);
@@ -84,35 +84,38 @@ const SentCellWidget = ({
         onConvert={(format: string) => {
           switch (format) {
             case "json":
-              const wbw: IWbw[] = data.content.split("\n").map((item, id) => {
-                const parts = item.split("=");
-                const word = my_to_roman(parts[0]);
-                const meaning: string = parts.length > 1 ? parts[1].trim() : "";
-                let parent: string = "";
-                let factors: string = "";
-                if (!meaning.includes(" ") && endings) {
-                  const base = nissayaBase(meaning, endings);
-                  parent = base.base;
-                  const end = base.ending ? base.ending : [];
-                  factors = [base.base, ...end].join("+");
-                } else {
-                  factors = meaning.replaceAll(" ", "+");
-                }
-                return {
-                  book: data.book,
-                  para: data.para,
-                  sn: [id],
-                  word: { value: word ? word : parts[0], status: 0 },
-                  real: { value: meaning, status: 0 },
-                  meaning: { value: "", status: 0 },
-                  parent: { value: parent, status: 0 },
-                  factors: {
-                    value: factors,
-                    status: 0,
-                  },
-                  confidence: 0.5,
-                };
-              });
+              const wbw: IWbw[] = sentData.content
+                ? sentData.content.split("\n").map((item, id) => {
+                    const parts = item.split("=");
+                    const word = my_to_roman(parts[0]);
+                    const meaning: string =
+                      parts.length > 1 ? parts[1].trim() : "";
+                    let parent: string = "";
+                    let factors: string = "";
+                    if (!meaning.includes(" ") && endings) {
+                      const base = nissayaBase(meaning, endings);
+                      parent = base.base;
+                      const end = base.ending ? base.ending : [];
+                      factors = [base.base, ...end].join("+");
+                    } else {
+                      factors = meaning.replaceAll(" ", "+");
+                    }
+                    return {
+                      book: sentData.book,
+                      para: sentData.para,
+                      sn: [id],
+                      word: { value: word ? word : parts[0], status: 0 },
+                      real: { value: meaning, status: 0 },
+                      meaning: { value: "", status: 0 },
+                      parent: { value: parent, status: 0 },
+                      factors: {
+                        value: factors,
+                        status: 0,
+                      },
+                      confidence: 0.5,
+                    };
+                  })
+                : [];
               setSentData((origin) => {
                 origin.contentType = "json";
                 origin.content = JSON.stringify(wbw);
@@ -123,7 +126,9 @@ const SentCellWidget = ({
               break;
             case "markdown":
               setSentData((origin) => {
-                const wbwData: IWbw[] = JSON.parse(origin.content);
+                const wbwData: IWbw[] = origin.content
+                  ? JSON.parse(origin.content)
+                  : [];
                 const newContent = wbwData
                   .map((item) => {
                     return [
@@ -143,41 +148,47 @@ const SentCellWidget = ({
           }
         }}
       >
-        <Space
-          direction={compact ? "horizontal" : "vertical"}
-          style={{ alignItems: "flex-start" }}
+        <div
+          style={{
+            display: "flex",
+            flexDirection: compact ? "row" : "column",
+            alignItems: "flex-start",
+          }}
         >
           <EditInfo data={sentData} compact={compact} />
           {isEditMode ? (
-            <div>
-              {sentData.contentType === "json" ? (
-                <SentWbwEdit
-                  data={sentData}
-                  onClose={() => {
-                    setIsEditMode(false);
-                  }}
-                  onSave={(data: ISentence) => {
-                    setSentData(data);
-                  }}
-                />
-              ) : (
-                <SentCellEditable
-                  data={sentData}
-                  isPr={isPr}
-                  onClose={() => {
-                    setIsEditMode(false);
-                  }}
-                  onSave={(data: ISentence) => {
-                    setSentData(data);
-                    setIsEditMode(false);
-                  }}
-                />
-              )}
-            </div>
+            sentData.contentType === "json" ? (
+              <SentWbwEdit
+                data={sentData}
+                onClose={() => {
+                  setIsEditMode(false);
+                }}
+                onSave={(data: ISentence) => {
+                  setSentData(data);
+                }}
+              />
+            ) : (
+              <SentCellEditable
+                data={sentData}
+                isPr={isPr}
+                onClose={() => {
+                  setIsEditMode(false);
+                }}
+                onSave={(data: ISentence) => {
+                  setSentData(data);
+                  setIsEditMode(false);
+                }}
+              />
+            )
           ) : (
             <MdView
-              style={{ marginLeft: compact ? 0 : "2em" }}
-              html={sentData.html !== "" ? sentData.html : "请输入"}
+              style={{
+                width: "100%",
+                paddingLeft: compact ? 0 : "2em",
+                marginBottom: 0,
+              }}
+              placeholder="请输入"
+              html={sentData.html}
               wordWidget={wordWidget}
             />
           )}
@@ -189,7 +200,7 @@ const SentCellWidget = ({
             prOpen={prOpen}
             onPrClose={() => setPrOpen(false)}
           />
-        </Space>
+        </div>
       </SentEditMenu>
       {compact ? undefined : <Divider style={{ margin: "10px 0" }} />}
     </div>

+ 9 - 2
dashboard/src/components/template/SentEdit/SentCellEditable.tsx

@@ -14,6 +14,7 @@ import { ISentence } from "../SentEdit";
 import TermTextArea from "../../general/TermTextArea";
 import { useAppSelector } from "../../../hooks";
 import { wordList } from "../../../reducers/sent-word";
+import Builder from "../Builder/Builder";
 
 const { Text } = Typography;
 
@@ -44,6 +45,9 @@ const SentCellEditableWidget = ({
 
   const savePr = () => {
     setSaving(true);
+    if (!value) {
+      return;
+    }
     post<ISentencePrRequest, ISentencePrResponse>(`/v2/sentpr`, {
       book: data.book,
       para: data.para,
@@ -118,9 +122,9 @@ const SentCellEditableWidget = ({
   };
 
   return (
-    <Typography.Paragraph>
+    <Typography.Paragraph style={{ width: "100%" }}>
       <TermTextArea
-        value={value}
+        value={value ? value : ""}
         menuOptions={termList}
         onChange={(value: string) => {
           setValue(value);
@@ -158,6 +162,9 @@ const SentCellEditableWidget = ({
               new line
             </Button>
           </span>
+          <Text keyboard style={{ cursor: "pointer" }}>
+            <Builder trigger={"<t>"} />
+          </Text>
         </div>
         <div>
           <Text keyboard>Ctrl/⌘</Text>➕<Text keyboard>enter</Text>=

+ 21 - 7
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -5,8 +5,9 @@ import { useAppSelector } from "../../../hooks";
 import { settingInfo } from "../../../reducers/setting";
 import { useEffect, useLayoutEffect, useRef, useState } from "react";
 import { GetUserSetting } from "../../auth/setting/default";
-import { mode } from "../../../reducers/article-mode";
+import { mode as _mode } from "../../../reducers/article-mode";
 import { IWbw } from "../Wbw/WbwWord";
+import { ArticleMode } from "../../article/Article";
 
 interface ILayoutFlex {
   left: number;
@@ -24,6 +25,7 @@ interface IWidgetSentContent {
   layout?: TDirection;
   magicDict?: string;
   compact?: boolean;
+  mode?: ArticleMode;
   onWbwChange?: Function;
   onMagicDictDone?: Function;
 }
@@ -37,6 +39,7 @@ const SentContentWidget = ({
   translation,
   layout = "column",
   compact = false,
+  mode,
   magicDict,
   onWbwChange,
   onMagicDictDone,
@@ -66,9 +69,19 @@ const SentContentWidget = ({
     }
   }, [settings]);
 
-  const newMode = useAppSelector(mode);
+  const newMode = useAppSelector(_mode);
+
   useEffect(() => {
-    switch (newMode) {
+    console.log("mode", mode);
+    let currMode: ArticleMode;
+    if (typeof mode !== "undefined") {
+      currMode = mode;
+    } else if (typeof newMode !== "undefined") {
+      currMode = newMode;
+    } else {
+      return;
+    }
+    switch (currMode) {
       case "edit":
         setLayoutFlex({
           left: 5,
@@ -82,7 +95,7 @@ const SentContentWidget = ({
         });
         break;
     }
-  }, [newMode]);
+  }, [mode, newMode]);
 
   useLayoutEffect(() => {
     const width = divShell.current?.offsetWidth;
@@ -119,7 +132,8 @@ const SentContentWidget = ({
                 wordEnd={wordEnd}
                 magicDict={magicDict}
                 channelId={item.channel.id}
-                data={JSON.parse(item.content)}
+                data={JSON.parse(item.content ? item.content : "")}
+                mode={mode}
                 onChange={(data: IWbw[]) => {
                   if (typeof onWbwChange !== "undefined") {
                     onWbwChange(data);
@@ -133,13 +147,13 @@ const SentContentWidget = ({
               />
             );
           } else {
-            return <SentCell key={id} data={item} wordWidget={true} />;
+            return <SentCell key={id} initValue={item} wordWidget={true} />;
           }
         })}
       </div>
       <div style={{ flex: layoutFlex.right }}>
         {translation?.map((item, id) => {
-          return <SentCell key={id} data={item} compact={compact} />;
+          return <SentCell key={id} initValue={item} compact={compact} />;
         })}
       </div>
     </div>

+ 13 - 5
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -13,6 +13,7 @@ import type { MenuProps } from "antd";
 import { ISentence } from "../SentEdit";
 import SentHistoryModal from "../../corpus/SentHistoryModal";
 import { HandOutlinedIcon, JsonOutlinedIcon } from "../../../assets/icon";
+import { useIntl } from "react-intl";
 
 interface IWidget {
   data: ISentence;
@@ -30,6 +31,7 @@ const SentEditMenuWidget = ({
 }: IWidget) => {
   const [isHover, setIsHover] = useState(false);
   const [timelineOpen, setTimelineOpen] = useState(false);
+  const intl = useIntl();
 
   const onClick: MenuProps["onClick"] = (e) => {
     if (typeof onMenuClick !== "undefined") {
@@ -92,7 +94,9 @@ const SentEditMenuWidget = ({
     },
     {
       key: "copy-link",
-      label: "复制链接",
+      label: intl.formatMessage({
+        id: "buttons.copy.link",
+      }),
       icon: <LinkOutlined />,
     },
   ];
@@ -113,7 +117,7 @@ const SentEditMenuWidget = ({
       />
       <div
         style={{
-          marginTop: "-4.2em",
+          marginTop: 0,
           right: 30,
           position: "absolute",
           display: isHover ? "block" : "none",
@@ -133,9 +137,13 @@ const SentEditMenuWidget = ({
           icon={<CopyOutlined />}
           size="small"
           onClick={() => {
-            navigator.clipboard.writeText(data.content).then(() => {
-              message.success("已经拷贝到剪贴板");
-            });
+            if (data.content) {
+              navigator.clipboard.writeText(data.content).then(() => {
+                message.success("已经拷贝到剪贴板");
+              });
+            } else {
+              message.success("内容为空");
+            }
           }}
         />
         <Dropdown menu={{ items, onClick }} placement="bottomRight">

+ 45 - 7
dashboard/src/components/template/SentEdit/SentMenu.tsx

@@ -1,26 +1,31 @@
+import { useIntl } from "react-intl";
 import { Badge, Button, Dropdown, Space } from "antd";
-import { MoreOutlined } from "@ant-design/icons";
+import { MoreOutlined, CheckOutlined } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 import RelatedPara from "../../corpus/RelatedPara";
+import { ArticleMode } from "../../article/Article";
 
-interface ISentMenu {
+interface IWidget {
   book?: number;
   para?: number;
   loading?: boolean;
+  mode?: ArticleMode;
   onMagicDict?: Function;
   onMenuClick?: Function;
 }
 const SentMenuWidget = ({
   book,
   para,
+  mode,
   loading = false,
   onMagicDict,
   onMenuClick,
-}: ISentMenu) => {
+}: IWidget) => {
+  const intl = useIntl();
   const items: MenuProps["items"] = [
     {
       key: "magic-dict-current",
-      label: "神奇字典",
+      label: intl.formatMessage({ id: "buttons.magic-dict" }),
     },
     {
       key: "show-commentary",
@@ -32,20 +37,53 @@ const SentMenuWidget = ({
     },
     {
       key: "copy-id",
-      label: "复制句子编号",
+      label: intl.formatMessage({ id: "buttons.copy.id" }),
     },
     {
       key: "copy-link",
-      label: "复制句子链接",
+      label: intl.formatMessage({ id: "buttons.copy.link" }),
     },
     {
       type: "divider",
     },
+    {
+      key: "origin",
+      label: "原文模式",
+      children: [
+        {
+          key: "origin-auto",
+          label: "自动",
+          icon: (
+            <CheckOutlined
+              style={{ visibility: mode === undefined ? "visible" : "hidden" }}
+            />
+          ),
+        },
+        {
+          key: "origin-edit",
+          label: "翻译",
+          icon: (
+            <CheckOutlined
+              style={{ visibility: mode === "edit" ? "visible" : "hidden" }}
+            />
+          ),
+        },
+        {
+          key: "origin-wbw",
+          label: "逐词",
+          icon: (
+            <CheckOutlined
+              style={{ visibility: mode === "wbw" ? "visible" : "hidden" }}
+            />
+          ),
+        },
+      ],
+    },
     {
       key: "compact",
       label: (
         <Space>
-          {"紧凑"}
+          {intl.formatMessage({ id: "buttons.compact" })}
           <Badge count="Beta" showZero color="#faad14" />
         </Space>
       ),

+ 211 - 163
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -14,6 +14,8 @@ import TocPath, { ITocPathNode } from "../../corpus/TocPath";
 import { IWbw } from "../Wbw/WbwWord";
 import RelaGraphic from "../Wbw/RelaGraphic";
 import SentMenu from "./SentMenu";
+import { ArticleMode } from "../../article/Article";
+import { IResNumber } from "../SentEdit";
 
 const { Text } = Typography;
 
@@ -34,8 +36,11 @@ interface IWidget {
   wbwData?: IWbw[];
   magicDictLoading?: boolean;
   compact?: boolean;
+  mode?: ArticleMode;
+  loadedRes?: IResNumber;
   onMagicDict?: Function;
   onCompact?: Function;
+  onModeChange?: Function;
 }
 const SentTabWidget = ({
   id,
@@ -53,11 +58,16 @@ const SentTabWidget = ({
   wbwData,
   magicDictLoading = false,
   compact = false,
+  mode,
+  loadedRes,
   onMagicDict,
   onCompact,
+  onModeChange,
 }: IWidget) => {
   const intl = useIntl();
   const [isCompact, setIsCompact] = useState(compact);
+  const [hover, setHover] = useState(false);
+  const [currKey, setCurrKey] = useState("close");
   useEffect(() => setIsCompact(compact), [compact]);
   const mPath = path
     ? [
@@ -70,144 +80,182 @@ const SentTabWidget = ({
   }
   const sentId = id.split("_");
   const sId = sentId[0].split("-");
+  const tabButtonStyle: React.CSSProperties | undefined = compact
+    ? { visibility: hover || currKey !== "close" ? "visible" : "hidden" }
+    : undefined;
 
   return (
-    <>
-      <Tabs
-        style={
-          isCompact
-            ? {
-                position: "absolute",
-                marginTop: -32,
-                width: "100%",
-                marginRight: 10,
+    <Tabs
+      onMouseEnter={() => setHover(true)}
+      onMouseLeave={() => setHover(false)}
+      activeKey={currKey}
+      onChange={(activeKey: string) => {
+        setCurrKey(activeKey);
+      }}
+      style={
+        isCompact
+          ? {
+              position: currKey === "close" ? "absolute" : "unset",
+              marginTop: -32,
+              width: "100%",
+              marginRight: 10,
+              backgroundColor: hover || currKey !== "close" ? "white" : "unset",
+            }
+          : undefined
+      }
+      tabBarStyle={{ marginBottom: 0 }}
+      size="small"
+      tabBarGutter={0}
+      tabBarExtraContent={
+        <Space>
+          <TocPath
+            link="none"
+            data={mPath}
+            trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
+          />
+          <Text copyable={{ text: `{{${sentId[0]}}}` }}>{sentId[0]}</Text>
+          <SentMenu
+            book={book}
+            para={para}
+            loading={magicDictLoading}
+            mode={mode}
+            onMagicDict={(type: string) => {
+              if (typeof onMagicDict !== "undefined") {
+                onMagicDict(type);
               }
-            : undefined
-        }
-        tabBarStyle={{ marginBottom: 0 }}
-        size="small"
-        tabBarGutter={0}
-        tabBarExtraContent={
-          <Space>
-            <TocPath
-              link="none"
-              data={mPath}
-              trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
-            />
-            <Text copyable={{ text: `{{${sentId[0]}}}` }}>{sentId[0]}</Text>
-            <SentMenu
-              book={book}
-              para={para}
-              loading={magicDictLoading}
-              onMagicDict={(type: string) => {
-                if (typeof onMagicDict !== "undefined") {
-                  onMagicDict(type);
-                }
-              }}
-              onMenuClick={(key: string) => {
-                switch (key) {
-                  case "compact" || "normal":
-                    if (typeof onCompact !== "undefined") {
-                      setIsCompact(true);
-                      onCompact(true);
-                    }
-                    break;
-                  case "normal":
-                    if (typeof onCompact !== "undefined") {
-                      setIsCompact(false);
-                      onCompact(false);
-                    }
-                    break;
-                  default:
-                    break;
-                }
-              }}
-            />
-          </Space>
-        }
-        items={[
-          {
-            label: (
+            }}
+            onMenuClick={(key: string) => {
+              switch (key) {
+                case "compact" || "normal":
+                  if (typeof onCompact !== "undefined") {
+                    setIsCompact(true);
+                    onCompact(true);
+                  }
+                  break;
+                case "normal":
+                  if (typeof onCompact !== "undefined") {
+                    setIsCompact(false);
+                    onCompact(false);
+                  }
+                  break;
+                case "origin-edit":
+                  if (typeof onModeChange !== "undefined") {
+                    onModeChange("edit");
+                  }
+                  break;
+                case "origin-wbw":
+                  if (typeof onModeChange !== "undefined") {
+                    onModeChange("wbw");
+                  }
+                  break;
+                default:
+                  break;
+              }
+            }}
+          />
+        </Space>
+      }
+      items={[
+        {
+          label: (
+            <span style={tabButtonStyle}>
               <Badge size="small" count={0}>
                 <CloseOutlined />
               </Badge>
-            ),
-            key: "close",
-            children: <></>,
-          },
-          {
-            label: (
-              <SentTabButton
-                icon={<TranslationOutlined />}
-                type="translation"
-                sentId={id}
-                count={tranNum}
-                title={intl.formatMessage({
-                  id: "channel.type.translation.label",
-                })}
-              />
-            ),
-            key: "translation",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="translation"
-                channelsId={channelsId}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                icon={<CloseOutlined />}
-                type="nissaya"
-                sentId={id}
-                count={nissayaNum}
-                title={intl.formatMessage({
-                  id: "channel.type.nissaya.label",
-                })}
-              />
-            ),
-            key: "nissaya",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="nissaya"
-                channelsId={channelsId}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                icon={<TranslationOutlined />}
-                type="commentary"
-                sentId={id}
-                count={commNum}
-                title={intl.formatMessage({
-                  id: "channel.type.commentary.label",
-                })}
-              />
-            ),
-            key: "commentary",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="commentary"
-                channelsId={channelsId}
-              />
-            ),
-          },
-          /*{
+            </span>
+          ),
+          key: "close",
+          children: <></>,
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<TranslationOutlined />}
+              type="translation"
+              sentId={id}
+              count={
+                tranNum
+                  ? tranNum -
+                    (loadedRes?.translation ? loadedRes.translation : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.translation.label",
+              })}
+            />
+          ),
+          key: "translation",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="translation"
+              channelsId={channelsId}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<CloseOutlined />}
+              type="nissaya"
+              sentId={id}
+              count={
+                nissayaNum
+                  ? nissayaNum - (loadedRes?.nissaya ? loadedRes.nissaya : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.nissaya.label",
+              })}
+            />
+          ),
+          key: "nissaya",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="nissaya"
+              channelsId={channelsId}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<TranslationOutlined />}
+              type="commentary"
+              sentId={id}
+              count={
+                commNum
+                  ? commNum - (loadedRes?.commentary ? loadedRes.commentary : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.commentary.label",
+              })}
+            />
+          ),
+          key: "commentary",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="commentary"
+              channelsId={channelsId}
+            />
+          ),
+        },
+        /*{
             label: (
               <SentTabButton
                 icon={<BlockOutlined />}
@@ -230,37 +278,37 @@ const SentTabWidget = ({
               />
             ),
           },*/
-          {
-            label: (
-              <SentTabButton
-                icon={<BlockOutlined />}
-                type="original"
-                sentId={id}
-                count={simNum}
-                title={intl.formatMessage({
-                  id: "buttons.sim",
-                })}
-              />
-            ),
-            key: "sim",
-            children: (
-              <SentSim
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                limit={5}
-              />
-            ),
-          },
-          {
-            label: "关系图",
-            key: "relation-graphic",
-            children: <RelaGraphic wbwData={wbwData} />,
-          },
-        ]}
-      />
-    </>
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<BlockOutlined />}
+              type="original"
+              sentId={id}
+              count={simNum}
+              title={intl.formatMessage({
+                id: "buttons.sim",
+              })}
+            />
+          ),
+          key: "sim",
+          children: (
+            <SentSim
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              limit={5}
+            />
+          ),
+        },
+        {
+          label: <span style={tabButtonStyle}>{"关系图"}</span>,
+          key: "relation-graphic",
+          children: <RelaGraphic wbwData={wbwData} />,
+        },
+      ]}
+    />
   );
 };
 

+ 6 - 1
dashboard/src/components/template/SentEdit/SentTabButton.tsx

@@ -14,6 +14,7 @@ const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
 };
 
 interface IWidget {
+  style?: React.CSSProperties;
   icon?: JSX.Element;
   type: string;
   sentId: string;
@@ -21,6 +22,7 @@ interface IWidget {
   title?: string;
 }
 const SentTabButtonWidget = ({
+  style,
   icon,
   type,
   sentId,
@@ -35,7 +37,9 @@ const SentTabButtonWidget = ({
       icon: <CalendarOutlined />,
     },
     {
-      label: "复制链接",
+      label: intl.formatMessage({
+        id: "buttons.copy.link",
+      }),
       key: "copyLink",
       icon: <LinkOutlined />,
     },
@@ -62,6 +66,7 @@ const SentTabButtonWidget = ({
 
   return (
     <Dropdown.Button
+      style={style}
       size="small"
       type="text"
       menu={menuProps}

+ 1 - 1
dashboard/src/components/template/SentEdit/SentWbwEdit.tsx

@@ -66,7 +66,7 @@ const SentWbwEditWidget = ({ data, onSave, onClose }: IWidget) => {
   const [wbwData, setWbwData] = useState<IWbw[]>([]);
 
   useEffect(() => {
-    if (data.contentType === "json") {
+    if (data.contentType === "json" && data.content) {
       setWbwData(JSON.parse(data.content));
     }
   }, [data.content, data.contentType]);

+ 15 - 13
dashboard/src/components/template/SentEdit/SuggestionAdd.tsx

@@ -29,19 +29,21 @@ const SuggestionAddWidget = ({ data, onCreate }: IWidget) => {
           添加修改建议
         </Button>
       </div>
-      <div style={{ display: isEditMode ? "block" : "none" }}>
-        <SentCellEditable
-          data={sentData}
-          isPr={true}
-          onClose={() => {
-            setIsEditMode(false);
-          }}
-          onCreate={() => {
-            if (typeof onCreate !== "undefined") {
-              onCreate();
-            }
-          }}
-        />
+      <div>
+        {isEditMode ? (
+          <SentCellEditable
+            data={sentData}
+            isPr={true}
+            onClose={() => {
+              setIsEditMode(false);
+            }}
+            onCreate={() => {
+              if (typeof onCreate !== "undefined") {
+                onCreate();
+              }
+            }}
+          />
+        ) : undefined}
       </div>
     </>
   );

+ 103 - 43
dashboard/src/components/template/SentEdit/SuggestionBox.tsx

@@ -6,6 +6,71 @@ import SuggestionAdd from "./SuggestionAdd";
 import { ISentence } from "../SentEdit";
 import Marked from "../../general/Marked";
 
+interface ISuggestionWidget {
+  data: ISentence;
+  openNotification: boolean;
+  enable?: boolean;
+  onNotificationChange?: Function;
+  onPrChange?: Function;
+}
+const Suggestion = ({
+  data,
+  enable = true,
+  openNotification,
+  onNotificationChange,
+  onPrChange,
+}: ISuggestionWidget) => {
+  const [reload, setReload] = useState(false);
+  return (
+    <Space direction="vertical" style={{ width: "100%" }}>
+      <Card
+        title="温馨提示"
+        size="small"
+        style={{
+          width: "100%",
+          display: openNotification ? "block" : "none",
+        }}
+      >
+        <Marked
+          text="此处专为提交修改建议译文。您输入的应该是**译文**
+  而不是评论和问题。其他内容,请在讨论页面提交。"
+        />
+        <p style={{ textAlign: "center" }}>
+          <Button
+            onClick={() => {
+              localStorage.setItem("read_pr_Notification", "ok");
+              if (typeof onNotificationChange !== "undefined") {
+                onNotificationChange(false);
+              }
+            }}
+          >
+            知道了
+          </Button>
+        </p>
+      </Card>
+      <SuggestionAdd
+        data={data}
+        onCreate={() => {
+          setReload(true);
+        }}
+      />
+      <SuggestionList
+        {...data}
+        enable={enable}
+        reload={reload}
+        onReload={() => {
+          setReload(false);
+        }}
+        onChange={(count: number) => {
+          if (typeof onPrChange !== "undefined") {
+            onPrChange(count);
+          }
+        }}
+      />
+    </Space>
+  );
+};
+
 export interface IAnswerCount {
   id: string;
   count: number;
@@ -14,16 +79,17 @@ interface IWidget {
   data: ISentence;
   trigger?: JSX.Element;
   open?: boolean;
+  openInDrawer?: boolean;
   onClose?: Function;
 }
 const SuggestionBoxWidget = ({
   trigger,
   data,
   open = false,
+  openInDrawer = true,
   onClose,
 }: IWidget) => {
   const [isOpen, setIsOpen] = useState(open);
-  const [reload, setReload] = useState(false);
   const [openNotification, setOpenNotification] = useState(false);
   const [prNumber, setPrNumber] = useState(data.suggestionCount?.suggestion);
 
@@ -53,51 +119,45 @@ const SuggestionBoxWidget = ({
         {prNumber}
       </Space>
 
-      <Drawer
-        title="修改建议"
-        width={520}
-        onClose={onBoxClose}
-        open={isOpen}
-        maskClosable={false}
-      >
-        <Space direction="vertical" style={{ width: "100%" }}>
-          <Card
-            title="温馨提示"
-            size="small"
-            style={{
-              width: "100%",
-              display: openNotification ? "block" : "none",
-            }}
-          >
-            <Marked
-              text="此处专为提交修改建议译文。您输入的应该是**译文**
-              而不是评论和问题。其他内容,请在讨论页面提交。"
-            />
-            <p style={{ textAlign: "center" }}>
-              <Button
-                onClick={() => {
-                  localStorage.setItem("read_pr_Notification", "ok");
-                  setOpenNotification(false);
-                }}
-              >
-                知道了
-              </Button>
-            </p>
-          </Card>
-          <SuggestionAdd
+      {openInDrawer ? (
+        <Drawer
+          title="修改建议"
+          width={520}
+          onClose={onBoxClose}
+          open={isOpen}
+          maskClosable={false}
+        >
+          <Suggestion
             data={data}
-            onCreate={() => {
-              setReload(true);
-            }}
+            enable={isOpen}
+            openNotification={openNotification}
+            onNotificationChange={(value: boolean) =>
+              setOpenNotification(value)
+            }
+            onPrChange={(value: number) => setPrNumber(value)}
           />
-          <SuggestionList
-            {...data}
-            reload={reload}
-            onReload={() => setReload(false)}
-            onChange={(count: number) => setPrNumber(count)}
+        </Drawer>
+      ) : (
+        <div
+          style={{
+            position: "absolute",
+            display: isOpen ? "none" : "none",
+            zIndex: 1030,
+            marginLeft: 300,
+            marginTop: -250,
+          }}
+        >
+          <Suggestion
+            data={data}
+            enable={isOpen}
+            openNotification={openNotification}
+            onNotificationChange={(value: boolean) =>
+              setOpenNotification(value)
+            }
+            onPrChange={(value: number) => setPrNumber(value)}
           />
-        </Space>
-      </Drawer>
+        </div>
+      )}
     </>
   );
 };

+ 9 - 5
dashboard/src/components/template/SentEdit/SuggestionList.tsx

@@ -12,6 +12,7 @@ interface IWidget {
   wordStart: number;
   wordEnd: number;
   channel: IChannel;
+  enable?: boolean;
   reload?: boolean;
   onReload?: Function;
   onChange?: Function;
@@ -23,18 +24,21 @@ const SuggestionListWidget = ({
   wordEnd,
   channel,
   reload = false,
+  enable = true,
   onReload,
   onChange,
 }: IWidget) => {
   const [sentData, setSentData] = useState<ISentence[]>([]);
 
   const load = () => {
-    get<ISuggestionListResponse>(
-      `/v2/sentpr?view=sent-info&book=${book}&para=${para}&start=${wordStart}&end=${wordEnd}&channel=${channel.id}`
-    )
+    if (!enable) {
+      return;
+    }
+    const url = `/v2/sentpr?view=sent-info&book=${book}&para=${para}&start=${wordStart}&end=${wordEnd}&channel=${channel.id}`;
+    console.log("url", url);
+    get<ISuggestionListResponse>(url)
       .then((json) => {
         if (json.ok) {
-          console.log("pr load", json.data.rows);
           const newData: ISentence[] = json.data.rows.map((item) => {
             return {
               id: item.id,
@@ -74,7 +78,7 @@ const SuggestionListWidget = ({
   return (
     <>
       {sentData.map((item, id) => {
-        return <SentCell data={item} key={id} isPr={true} />;
+        return <SentCell initValue={item} key={id} isPr={true} />;
       })}
     </>
   );

+ 3 - 1
dashboard/src/components/template/SentEdit/SuggestionToolbar.tsx

@@ -75,7 +75,9 @@ const SuggestionToolbarWidget = ({
       />
       {CommentCount}
       {compact ? undefined : <Divider type="vertical" />}
-      {compact ? undefined : <Text copyable={{ text: data.content }}></Text>}
+      {compact ? undefined : (
+        <Text copyable={{ text: data.content ? data.content : "" }}></Text>
+      )}
     </Space>
   );
   return (

+ 2 - 4
dashboard/src/components/template/Term.tsx

@@ -1,17 +1,16 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
 import { Button, Popover, Skeleton, Space } from "antd";
 import { Typography } from "antd";
 import { SearchOutlined, EditOutlined } from "@ant-design/icons";
-import { ProCard } from "@ant-design/pro-components";
 
 import store from "../../store";
 import TermModal from "../term/TermModal";
 import { ITerm } from "../term/TermEdit";
-import { useEffect, useState } from "react";
 import { ITermDataResponse } from "../api/Term";
 import { changedTerm, refresh } from "../../reducers/term-change";
 import { useAppSelector } from "../../hooks";
 import { get } from "../../request";
-import { Link, useNavigate } from "react-router-dom";
 import { fullUrl } from "../../utils";
 
 const { Text, Title } = Typography;
@@ -47,7 +46,6 @@ const TermCtl = ({
   const [openPopover, setOpenPopover] = useState(false);
   const [termData, setTermData] = useState<ITerm>();
   const [content, setContent] = useState<string>();
-  const navigate = useNavigate();
   const newTerm: ITermDataResponse | undefined = useAppSelector(changedTerm);
 
   useEffect(() => {

+ 25 - 17
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -1,10 +1,11 @@
 import { useEffect, useState } from "react";
-import { Button, Popover, Space, Typography } from "antd";
+import { Button, Popover, Space, Tag, Typography } from "antd";
 import {
   TagTwoTone,
   InfoCircleOutlined,
   CommentOutlined,
   ApartmentOutlined,
+  EditOutlined,
 } from "@ant-design/icons";
 
 import "./wbw.css";
@@ -119,14 +120,12 @@ const WbwPaliWidget = ({ data, mode, display, onSave }: IWidget) => {
   ]);
 
   const handleClickChange = (open: boolean) => {
-    if (mode === "wbw") {
-      if (open) {
-        setPaliColor("lightblue");
-      } else {
-        setPaliColor("unset");
-      }
-      setPopOpen(open);
+    if (open) {
+      setPaliColor("lightblue");
+    } else {
+      setPaliColor("unset");
     }
+    setPopOpen(open);
   };
 
   const wbwDetail = (
@@ -256,15 +255,24 @@ const WbwPaliWidget = ({ data, mode, display, onSave }: IWidget) => {
     //非标点符号
     return (
       <div className="pali_shell">
-        <Popover
-          content={wbwDetail}
-          placement="bottom"
-          trigger="click"
-          open={popOpen}
-          onOpenChange={handleClickChange}
-        >
-          {paliWord}
-        </Popover>
+        <span className="pali_shell_spell">
+          {mode === "edit" ? paliWord : ""}
+          <Popover
+            content={wbwDetail}
+            placement="bottom"
+            trigger="click"
+            open={popOpen}
+            onOpenChange={handleClickChange}
+          >
+            {mode === "wbw" ? (
+              paliWord
+            ) : (
+              <span className="edit_icon">
+                <EditOutlined style={{ cursor: "pointer" }} />
+              </span>
+            )}
+          </Popover>
+        </span>
         <Space>
           {videoIcon}
           {noteIcon}

+ 1 - 1
dashboard/src/components/template/Wbw/WbwReal.tsx

@@ -1,4 +1,4 @@
-import { Space, Tooltip } from "antd";
+import { Tooltip } from "antd";
 import { Typography } from "antd";
 import Lookup from "../../dict/Lookup";
 

+ 9 - 0
dashboard/src/components/template/Wbw/wbw.css

@@ -91,3 +91,12 @@
   margin: 0 0.3em 0 1px;
   padding: 0px 2px;
 }
+.edit_icon {
+  display: none;
+  position: absolute;
+  border: 1px solid gray;
+  background-color: wheat;
+}
+.pali_shell:hover .edit_icon {
+  display: inline-block;
+}

+ 18 - 6
dashboard/src/components/template/WbwSent.tsx

@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
 import { MoreOutlined } from "@ant-design/icons";
 
 import { useAppSelector } from "../../hooks";
-import { mode } from "../../reducers/article-mode";
+import { mode as _mode } from "../../reducers/article-mode";
 import { post } from "../../request";
 import { ArticleMode } from "../article/Article";
 import WbwWord, {
@@ -84,6 +84,7 @@ interface IWidget {
   layoutDirection?: "h" | "v";
   magicDict?: string;
   refreshable?: boolean;
+  mode?: ArticleMode;
   onMagicDictDone?: Function;
   onChange?: Function;
 }
@@ -98,6 +99,7 @@ export const WbwSentCtl = ({
   display = "block",
   fields,
   layoutDirection = "h",
+  mode,
   magicDict,
   refreshable = false,
   onChange,
@@ -116,7 +118,8 @@ export const WbwSentCtl = ({
     setMagic(magicDict);
   }, [magicDict]);
 
-  const newMode = useAppSelector(mode);
+  const newMode = useAppSelector(_mode);
+
   const update = (data: IWbw[]) => {
     setWordData(data);
     if (typeof onChange !== "undefined") {
@@ -127,7 +130,7 @@ export const WbwSentCtl = ({
     if (refreshable) {
       setWordData(data);
     }
-  }, [data]);
+  }, [data, refreshable]);
 
   useEffect(() => {
     //发布句子里面的单词的变更。给术语输入菜单用。
@@ -154,8 +157,17 @@ export const WbwSentCtl = ({
   }, [book, para, wordData, wordEnd, wordStart]);
 
   useEffect(() => {
-    setDisplayMode(newMode);
-    switch (newMode) {
+    console.log("mode", mode);
+    let currMode: ArticleMode | undefined;
+    if (typeof mode !== "undefined") {
+      currMode = mode;
+    } else if (typeof newMode !== "undefined") {
+      currMode = newMode;
+    } else {
+      currMode = undefined;
+    }
+    setDisplayMode(currMode);
+    switch (currMode) {
       case "edit":
         if (typeof display === "undefined") {
           setWbwMode("block");
@@ -185,7 +197,7 @@ export const WbwSentCtl = ({
         }
         break;
     }
-  }, [newMode]);
+  }, [newMode, mode]);
 
   useEffect(() => {
     if (typeof magic === "undefined") {

+ 0 - 2
dashboard/src/components/template/Wd.tsx

@@ -1,5 +1,3 @@
-import { useState } from "react";
-
 import { lookup } from "../../reducers/command";
 import store from "../../store";
 import "./style.css";

+ 17 - 4
dashboard/src/components/template/utilities.ts

@@ -7,6 +7,7 @@ import { roman_to_my, my_to_roman } from "../code/my";
 import { roman_to_si } from "../code/si";
 import { roman_to_thai } from "../code/thai";
 import { roman_to_taitham } from "../code/tai-tham";
+import ParserError from "../general/ParserError";
 
 export type TCodeConvertor =
   | "none"
@@ -24,10 +25,11 @@ export function XmlToReact(
   //console.log("html string:", text);
   const parser = new DOMParser();
   const xmlDoc = parser.parseFromString(
-    "<root>" + text + "</root>",
-    "text/xml"
+    `<body>${text}</body>`,
+    "application/xml"
   );
   const x = xmlDoc.documentElement;
+  //console.log("解析成功", x);
   return convert(x, wordWidget, convertor);
 
   function getAttr(node: ChildNode, key: number): Object {
@@ -59,7 +61,18 @@ export function XmlToReact(
 
       switch (value.nodeType) {
         case 1: //element node
-          switch (value.nodeName) {
+          //console.log("tag name", value.nodeName);
+          const tagName = value.nodeName;
+          switch (tagName) {
+            case "parsererror":
+              output.push(
+                React.createElement(
+                  ParserError,
+                  getAttr(value, i),
+                  convert(value, wordWidget, convertor)
+                )
+              );
+              break;
             case "MdTpl":
               output.push(
                 React.createElement(
@@ -90,7 +103,7 @@ export function XmlToReact(
             default:
               output.push(
                 React.createElement(
-                  value.nodeName,
+                  tagName,
                   getAttr(value, i),
                   convert(value, wordWidget, convertor)
                 )

+ 1 - 2
dashboard/src/components/term/TermItem.tsx

@@ -28,8 +28,7 @@ const TermItemWidget = ({ data }: IWidget) => {
               <Text type="secondary">
                 <UserName {...data?.editor} />
               </Text>
-              <Text type="secondary">update at</Text>
-              <TimeShow time={data?.updated_at} />
+              <TimeShow type="secondary" updatedAt={data?.updated_at} />
             </Space>
           </Space>
         }

+ 9 - 12
dashboard/src/components/term/TermList.tsx

@@ -19,6 +19,7 @@ import { IChannel } from "../channel/Channel";
 import TermExport from "./TermExport";
 import DataImport from "../admin/relation/DataImport";
 import TermModal from "./TermModal";
+import { getSorterUrl } from "../../utils";
 
 interface IItem {
   sn: number;
@@ -29,7 +30,7 @@ interface IItem {
   meaning: string;
   meaning2: string;
   note: string | null;
-  createdAt: number;
+  updated_at: string;
 }
 
 interface IWidget {
@@ -151,14 +152,14 @@ const TermListWidget = ({ studioName, channelId }: IWidget) => {
           },
           {
             title: intl.formatMessage({
-              id: "forms.fields.created-at.label",
+              id: "forms.fields.updated-at.label",
             }),
-            key: "created-at",
+            key: "updated_at",
             width: 200,
             search: false,
-            dataIndex: "createdAt",
+            dataIndex: "updated_at",
             valueType: "date",
-            sorter: (a, b) => a.createdAt - b.createdAt,
+            sorter: true,
           },
           {
             title: intl.formatMessage({ id: "buttons.option" }),
@@ -253,7 +254,6 @@ const TermListWidget = ({ studioName, channelId }: IWidget) => {
           );
         }}
         request={async (params = {}, sorter, filter) => {
-          // TODO
           console.log(params, sorter, filter);
           const offset =
             ((params.current ? params.current : 1) - 1) *
@@ -269,15 +269,12 @@ const TermListWidget = ({ studioName, channelId }: IWidget) => {
           if (typeof params.keyword !== "undefined") {
             url += "&search=" + (params.keyword ? params.keyword : "");
           }
-
+          url += getSorterUrl(sorter);
           const res = await get<ITermListResponse>(url);
           console.log(res);
           const items: IItem[] = res.data.rows.map((item, id) => {
-            const date = new Date(item.updated_at);
-            const id2 =
-              ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
             return {
-              sn: id2,
+              sn: id + offset + 1,
               id: item.guid,
               word: item.word,
               tag: item.tag,
@@ -285,7 +282,7 @@ const TermListWidget = ({ studioName, channelId }: IWidget) => {
               meaning: item.meaning,
               meaning2: item.other_meaning,
               note: item.note,
-              createdAt: date.getTime(),
+              updated_at: item.updated_at,
             };
           });
           return {

+ 160 - 0
dashboard/src/components/webhook/WebhookEdit.tsx

@@ -0,0 +1,160 @@
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormSelect,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { message, Space, Typography } from "antd";
+import { useRef } from "react";
+import { useIntl } from "react-intl";
+import { Link } from "react-router-dom";
+
+import { get, post, put } from "../../request";
+import { IWebhookRequest, IWebhookResponse } from "../api/webhook";
+import { TResType } from "../discussion/DiscussionListCard";
+
+const { Title } = Typography;
+
+interface IWidget {
+  studioName?: string;
+  channelId?: string;
+  id?: string;
+  res_type?: TResType;
+  res_id?: string;
+  onSuccess?: Function;
+}
+const WebhookEditWidget = ({
+  studioName,
+  channelId,
+  id,
+  res_type = "channel",
+  res_id = "",
+  onSuccess,
+}: IWidget) => {
+  const formRef = useRef<ProFormInstance>();
+  const intl = useIntl();
+
+  return (
+    <Space direction="vertical">
+      <Title level={4}>
+        <Link
+          to={`/studio/${studioName}/channel/${channelId}/setting/webhooks`}
+        >
+          List
+        </Link>{" "}
+        / {id ? "Manage webhook" : "New"}
+      </Title>
+      <ProForm<IWebhookRequest>
+        formRef={formRef}
+        autoFocusFirstInput
+        onFinish={async (values) => {
+          console.log("submit", values);
+          let data: IWebhookRequest = values;
+          data.res_id = res_id;
+          data.res_type = res_type;
+          let res: IWebhookResponse;
+          if (typeof id === "undefined") {
+            res = await post<IWebhookRequest, IWebhookResponse>(
+              `/v2/webhook`,
+              data
+            );
+          } else {
+            res = await put<IWebhookRequest, IWebhookResponse>(
+              `/v2/webhook/${id}`,
+              data
+            );
+          }
+          console.log(res);
+          if (res.ok) {
+            message.success("提交成功");
+            if (typeof onSuccess !== "undefined") {
+              onSuccess();
+            }
+          } else {
+            message.error(res.message);
+          }
+
+          return true;
+        }}
+        request={
+          id
+            ? async () => {
+                const url = `/v2/webhook/${id}`;
+                const res: IWebhookResponse = await get<IWebhookResponse>(url);
+                console.log("get", res);
+                if (res.ok) {
+                  return res.data;
+                } else {
+                  return {
+                    res_type: res_type,
+                    res_id: res_id,
+                    url: "",
+                    receiver: "wechat",
+                    event: [],
+                    status: "normal",
+                  };
+                }
+              }
+            : undefined
+        }
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            required
+            name="url"
+            label={intl.formatMessage({ id: "forms.fields.url.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            options={[
+              { value: "wechat", label: "wechat" },
+              { value: "dingtalk", label: "dingtalk" },
+            ]}
+            width="md"
+            required
+            name={"receiver"}
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.receiver.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            placeholder={"全部事件"}
+            options={["pr", "discussion", "content"].map((item) => {
+              return {
+                value: item,
+                label: item,
+              };
+            })}
+            fieldProps={{
+              mode: "tags",
+            }}
+            width="md"
+            name="event"
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.event.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            placeholder={"active"}
+            options={["active", "disable"].map((item) => {
+              return {
+                value: item,
+                label: item,
+              };
+            })}
+            width="md"
+            name="status"
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.status.label" })}
+          />
+        </ProForm.Group>
+      </ProForm>
+    </Space>
+  );
+};
+
+export default WebhookEditWidget;

+ 236 - 0
dashboard/src/components/webhook/WebhookList.tsx

@@ -0,0 +1,236 @@
+import { ActionType, ProTable } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { Link, useNavigate } from "react-router-dom";
+import { message, Modal, Space, Typography } from "antd";
+import { Button, Dropdown } from "antd";
+import {
+  PlusOutlined,
+  ExclamationCircleOutlined,
+  DeleteOutlined,
+  StopOutlined,
+  CheckCircleOutlined,
+  WarningOutlined,
+} from "@ant-design/icons";
+
+import { delete_, get } from "../../request";
+import { IDeleteResponse } from "../api/Article";
+import { useRef } from "react";
+import { IWebhookApiData, IWebhookListResponse } from "../api/webhook";
+
+const { Text } = Typography;
+
+interface IWidget {
+  channelId?: string;
+  studioName?: string;
+}
+
+const WebhookListWidget = ({ channelId, studioName }: IWidget) => {
+  const intl = useIntl();
+  const navigate = useNavigate();
+
+  const showDeleteConfirm = (id: string, title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.sure",
+        }) +
+        intl.formatMessage({
+          id: "message.irrevocable",
+        }),
+
+      content: title,
+      okText: intl.formatMessage({
+        id: "buttons.delete",
+      }),
+      okType: "danger",
+      cancelText: intl.formatMessage({
+        id: "buttons.no",
+      }),
+      onOk() {
+        console.log("delete", id);
+        return delete_<IDeleteResponse>(`/v2/webhook/${id}`)
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              ref.current?.reload();
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
+
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProTable<IWebhookApiData>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.url.label",
+            }),
+            dataIndex: "url",
+            key: "url",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              const url = row.url.split("?")[0];
+              return (
+                <Space>
+                  {row.status === "disable" ? (
+                    <StopOutlined style={{ color: "red" }} />
+                  ) : (
+                    <CheckCircleOutlined style={{ color: "green" }} />
+                  )}
+                  {url}
+                  <Text type="secondary" italic>
+                    <Space>{row.event}</Space>
+                  </Text>
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.receiver.label",
+            }),
+            dataIndex: "receiver",
+            key: "receiver",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.fail.label",
+            }),
+            dataIndex: "fail",
+            key: "fail",
+            width: 100,
+            search: false,
+            render: (text, row, index, action) => {
+              return (
+                <Space>
+                  {row.fail > 0 ? (
+                    <WarningOutlined style={{ color: "orange" }} />
+                  ) : undefined}
+                  {row.fail}
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.success.label",
+            }),
+            dataIndex: "success",
+            key: "success",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.status.label",
+            }),
+            dataIndex: "status",
+            key: "status",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button
+                  key={index}
+                  type="link"
+                  trigger={["click", "contextMenu"]}
+                  menu={{
+                    items: [
+                      {
+                        key: "remove",
+                        label: intl.formatMessage({
+                          id: "buttons.delete",
+                        }),
+                        icon: <DeleteOutlined />,
+                        danger: true,
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "remove":
+                          showDeleteConfirm(row.id, row.url);
+                          break;
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                >
+                  <Link
+                    to={`/studio/${studioName}/channel/${channelId}/setting/webhooks/${row.id}`}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          let url = `/v2/webhook?view=channel&id=${channelId}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          console.log("url", url);
+          const res: IWebhookListResponse = await get(url);
+
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: res.data.rows,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Button
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+            onClick={() =>
+              navigate(
+                `/studio/${studioName}/channel/${channelId}/setting/webhooks/new`
+              )
+            }
+          >
+            {intl.formatMessage({ id: "buttons.create" })}
+          </Button>,
+        ]}
+      />
+    </>
+  );
+};
+
+export default WebhookListWidget;

+ 6 - 0
dashboard/src/locales/zh-Hans/buttons.ts

@@ -53,6 +53,12 @@ const items = {
   "buttons.lookup": "查字典",
   "buttons.invite": "邀请",
   "buttons.relate.to": "关联到",
+  "buttons.copy.link": "复制链接",
+  "buttons.copy.id": "复制编号",
+  "buttons.reply": "回复",
+  "buttons.open": "打开",
+  "buttons.compact": "紧凑",
+  "buttons.magic-dict": "神奇字典",
 };
 
 export default items;

+ 2 - 2
dashboard/src/locales/zh-Hans/dict/index.ts

@@ -133,8 +133,8 @@ const items = {
   "dict.fields.type.none.short.label": "_",
   "dict.fields.type.null.label": "空",
   "dict.fields.type.null.short.label": "_",
-  "dict.fields.type.?.label": "?",
-  "dict.fields.type.?.short.label": "?",
+  "dict.fields.type.?.label": "待定",
+  "dict.fields.type.?.short.label": "待定",
   "dict.fields.type.ti:base.label": "三性词干",
   "dict.fields.type.ti:base.short.label": "三性词干",
   "dict.fields.type.n:base.label": "名词干",

+ 5 - 0
dashboard/src/locales/zh-Hans/forms.ts

@@ -72,6 +72,11 @@ const items = {
   "forms.fields.match.label": "匹配方式",
   "forms.fields.dict.name.label": "字典名",
   "forms.fields.dict.shortname.label": "字典名",
+  "forms.fields.url.label": "url",
+  "forms.fields.fail.label": "失败",
+  "forms.fields.success.label": "成功",
+  "forms.fields.receiver.label": "接收",
+  "forms.fields.event.label": "事件",
 };
 
 export default items;

Некоторые файлы не были показаны из-за большого количества измененных файлов