kirin-ri / memo

0 stars 0 forks source link

319 #15

Open kirin-ri opened 5 months ago

kirin-ri commented 5 months ago
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)\)/);
    return match ? parseInt(match[1], 10) : 1;
  };  
kirin-ri commented 5 months ago
  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)\)/);
    return match ? `#page=${match[1]}` : '';
  };
kirin-ri commented 5 months ago
  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)\)/);
    return match ? `#page=${match[1]}` : '';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)\)/);
    return match ? parseInt(match[1], 10) : 1;
  };  
kirin-ri commented 5 months ago
import React, { useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

    const loadPdf = async () => {
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        // 使用传入的pageNumber获取指定的PDF页面
        const page = await pdf.getPage(pageNumber);
        const scale = 0.5; // 调整缩放比例以适应预览大小
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]); // 添加pageNumber作为依赖项

  return <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;
kirin-ri commented 5 months ago

'extractPageNumberFromTitle' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

kirin-ri commented 5 months ago
import React, { useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

    const loadPdf = async () => {
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        // 使用传入的pageNumber获取指定的PDF页面
        const page = await pdf.getPage(pageNumber);
        const scale = 0.5; // 调整缩放比例以适应预览大小
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]); // 添加pageNumber作为依赖项

  return <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;
kirin-ri commented 5 months ago
import { IconFolderPlus, IconMistOff, IconPlus, IconFile } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import { Reference } from '@/types/reference';
import {
  CloseSidebarButton,
  OpenSidebarButton,
} from './components/OpenCloseButton';
import PDFPreview from '../Referencebar/PDFPreview';

// interface Reference {
//   id: string;
//   title: string;
//   description: string;
// }

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({
  isOpen,
  side,
  items,
  toggleOpen,
}: Props<T>) => {
  items.map((item,index) =>{
    console.log(item)
  })
  const { t } = useTranslation('promptbar');
  const [hoveredItemId, setHoveredItemId] = useState<number | null>(null);

  const allowDrop = (e: any) => {
    e.preventDefault();
  };

  const highlightDrop = (e: any) => {
    e.target.style.background = '#343541';
  };

  const removeHighlight = (e: any) => {
    e.target.style.background = 'none';
  };

  const titleStyle = {
    display: 'flex',
    alignItems: 'center',
    marginBottom: '30px',
    fontSize:'18px'
  }
  const headerStyle = {
    display: 'flex',
    justiftContent: 'space-between',
    alignItems:'center',
    fontSize: '15px',
    marginBottom: '15px',
    width: '100%',
  }
  const titleIcon = {
    display: 'flex',
    marginRight: '8px',
  }
  const listItemStyle = (isHovered: boolean) => ({
    marginBottom: '10px',
    border: '1px solid white',
    padding: '20px',
    borderRadius: '5px',
    backgroundColor: isHovered ? '#0056b3' : '#343541', // Change background color on hover
    transition: 'background-color 0.3s',
    maxWidth: '350px',
  });
  const listTitleStyle = {
    marginBottom: '10px',
  }
  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? `#page=${match[1]}` : '';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? parseInt(match[1], 10) : 1;
  };  
  const itemsample = [
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(30)',content:'日の原子力規制委員会に提出された論点のうちの残84された論点(例えば、新設炉と既設炉で目標値を分けるべきか否かなど)に関する議論を含め、安全目標に関する議論は、継続的な安全性向上を目指す原子力規制委員会として、今後とも引き続き検討を進めていくものとする。(3)新規制基準と安全目標の関係について原子力規制委員会は、安全目標は、基準ではなく規制を進めていく上で達成を目指す目標であると位置付けた。そして、原子炉等規制法の改正により新設された43条の3の29(発電用原子炉施設の安全性の向上のための評価)により、発電用原子炉設置者は施設定期検査終了後6ヶ月以内に自ら、安全性の向上のための評価を実施し、その結果を原子力規制委員会に届け出ることとなる。この安全性向上のための評価には、炉心損傷頻度、格納容器機能喪失頻度及びセシウム137の放出量が100テラベクレルを超えるような事故の発生頻度の評価が含まれており、原子力規制委員会は安全目標を参考にこの評価結果を踏まえ、必要な場合には、'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(20,32)',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf()',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  ]
  return isOpen ? (
    <div>
      <div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
          <div style={titleStyle}>
            <IconFile style={{ marginRight: '8px' }} />
            参照リスト
          </div>
          <div onClick={toggleOpen} style={titleStyle}>
            {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
          </div>
        </div>
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>
          <ul>
            {itemsample.map((item,index) => (
              <a href={`${item.source}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
                <li
                style={listItemStyle(index === hoveredItemId)}
                onMouseEnter={() => setHoveredItemId(index)}
                onMouseLeave={() => setHoveredItemId(null)}
                >
                  <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
                  <h3 style={listTitleStyle}>{item.title}</h3>
                  <p>{item.content}</p>
                  <div>test1 {extractPageNumberFromTitle(item.title)}</div>
                  <div>test2 {`${getPageNumberFromTitle(item.title)}`}</div>
                </li>
              </a>
            ))}
          </ul>
        </div>
      </div>
    </div>
  ) : (
    <OpenSidebarButton onClick={toggleOpen} side={side} />
  );
};

export default ReferenceSidebar;
kirin-ri commented 5 months ago
import React from 'react';
import PDFPreview from './PDFPreview'; // 假设您的PDF预览组件的路径

interface YourComponentProps {
  title: string;
  fileUrl: string;
  content: string;
}

const YourComponent: React.FC<YourComponentProps> = ({ title, fileUrl, content }) => {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
      {/* 标题部分 */}
      <h2>{title}</h2>

      {/* 内容布局 */}
      <div style={{ display: 'flex', width: '100%', height: 'auto' }}>
        {/* PDF预览部分 */}
        <div style={{ width: '30%' }}>
          <PDFPreview fileUrl={fileUrl} pageNumber={1} />
        </div>

        {/* 内容部分 */}
        <div style={{ width: '70%', paddingLeft: '20px' }}>
          {content}
        </div>
      </div>
    </div>
  );
};

export default YourComponent;
kirin-ri commented 5 months ago
const listItemStyle = (isHovered: boolean) => ({
  display: 'flex', // 应用Flex布局
  flexDirection: 'column', // 垂直排列
  marginBottom: '10px',
  border: '1px solid white',
  padding: '20px',
  borderRadius: '5px',
  backgroundColor: isHovered ? '#0056b3' : '#343541',
  transition: 'background-color 0.3s',
  maxWidth: '350px',
});

const listContentStyle = {
  display: 'flex', // 应用Flex布局
  flexDirection: 'row', // 水平排列
  width: '100%', // 占满容器宽度
};

const pdfPreviewStyle = {
  flex: '3', // 占据30%的空间
  marginRight: '20px', // 与内容部分间隔
};

const contentStyle = {
  flex: '7', // 占据剩余70%的空间
};
kirin-ri commented 5 months ago
    <ul>
      {itemsample.map((item, index) => (
        <a href={`${item.source}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
          <li
            style={listItemStyle(index === hoveredItemId)}
            onMouseEnter={() => setHoveredItemId(index)}
            onMouseLeave={() => setHoveredItemId(null)}
          >
            <div style={listContentStyle}>
              {/* PDF预览组件 */}
              <div style={pdfPreviewStyle}>
                <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
              </div>
              {/* 内容部分 */}
              <div style={contentStyle}>
                <h3 style={listTitleStyle}>{item.title}</h3>
                <p>{item.content}</p>
              </div>
            </div>
            <div>test1 {extractPageNumberFromTitle(item.title)}</div>
            <div>test2 {`${getPageNumberFromTitle(item.title)}`}</div>
          </li>
        </a>
      ))}
    </ul>
kirin-ri commented 5 months ago

Type '{ display: string; flexDirection: string; marginBottom: string; border: string; padding: string; borderRadius: string; backgroundColor: string; transition: string; maxWidth: string; }' is not assignable to type 'Properties<string | number, string & {}>'. Types of property 'flexDirection' are incompatible. Type 'string' is not assignable to type 'FlexDirection | undefined'.ts(2322)

kirin-ri commented 5 months ago
const contentStyle = {
  display: '-webkit-box',
  WebkitBoxOrient: 'vertical',
  WebkitLineClamp: 3, // 限制在3行内显示省略号,可根据需要调整行数
  overflow: 'hidden',
  textOverflow: 'ellipsis',
};

// PDF预览组件的样式
const pdfPreviewStyle = {
  width: '100%', // 宽度占满容器,或根据需求调整
  height: 'auto', // 高度自适应以保持宽高比
  objectFit: 'contain', // 保证内容按比例缩放以完全显示在给定区域内
};
kirin-ri commented 5 months ago
import { IconFolderPlus, IconMistOff, IconPlus, IconFile } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import { Reference } from '@/types/reference';
import {
  CloseSidebarButton,
  OpenSidebarButton,
} from './components/OpenCloseButton';
import PDFPreview from '../Referencebar/PDFPreview';

// interface Reference {
//   id: string;
//   title: string;
//   description: string;
// }

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({
  isOpen,
  side,
  items,
  toggleOpen,
}: Props<T>) => {
  items.map((item,index) =>{
    console.log(item)
  })
  const { t } = useTranslation('promptbar');
  const [hoveredItemId, setHoveredItemId] = useState<number | null>(null);

  const allowDrop = (e: any) => {
    e.preventDefault();
  };

  const highlightDrop = (e: any) => {
    e.target.style.background = '#343541';
  };

  const removeHighlight = (e: any) => {
    e.target.style.background = 'none';
  };

  const titleStyle = {
    display: 'flex',
    alignItems: 'center',
    marginBottom: '30px',
    fontSize:'18px'
  }
  const headerStyle = {
    display: 'flex',
    justiftContent: 'space-between',
    alignItems:'center',
    fontSize: '15px',
    marginBottom: '15px',
    width: '100%',
  }
  const titleIcon = {
    display: 'flex',
    marginRight: '8px',
  }

  const listTitleStyle = {
    marginBottom: '10px',
    width: '100',
  }
  const listItemStyle = (isHovered: boolean) => ({
    display: 'flex', 
    flexDirection: 'column' as 'column', 
    marginBottom: '10px',
    border: '1px solid white',
    padding: '20px',
    borderRadius: '5px',
    backgroundColor: isHovered ? '#0056b3' : '#343541',
    transition: 'background-color 0.3s',
    maxWidth: '350px',
  });

  const listContentStyle = {
    display: 'flex', // 应用Flex布局
    flexDirection: 'row' as 'row', // 水平排列
    width: '100%', // 占满容器宽度
  };

  const contentStyle = {
    display: '-webkit-box',
    WebkitBoxOrient: 'vertical' as 'vertical',
    WebkitLineClamp: 3, // 限制在3行内显示省略号,可根据需要调整行数
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  };

  // PDF预览组件的样式
  const pdfPreviewStyle = {
    width: '100%', // 宽度占满容器,或根据需求调整
    height: 'auto', // 高度自适应以保持宽高比
    objectFit: 'contain' as 'contain', // 保证内容按比例缩放以完全显示在给定区域内
  };
  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? `#page=${match[1]}` : '';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? parseInt(match[1], 10) : 1;
  };  
  const itemsample = [
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(30)',content:'日の原子力規制委員会に提出された論点のうちの残84された論点(例えば、新設炉と既設炉で目標値を分けるべきか否かなど)に関する議論を含め、安全目標に関する議論は、継続的な安全性向上を目指す原子力規制委員会として、今後とも引き続き検討を進めていくものとする。(3)新規制基準と安全目標の関係について原子力規制委員会は、安全目標は、基準ではなく規制を進めていく上で達成を目指す目標であると位置付けた。そして、原子炉等規制法の改正により新設された43条の3の29(発電用原子炉施設の安全性の向上のための評価)により、発電用原子炉設置者は施設定期検査終了後6ヶ月以内に自ら、安全性の向上のための評価を実施し、その結果を原子力規制委員会に届け出ることとなる。この安全性向上のための評価には、炉心損傷頻度、格納容器機能喪失頻度及びセシウム137の放出量が100テラベクレルを超えるような事故の発生頻度の評価が含まれており、原子力規制委員会は安全目標を参考にこの評価結果を踏まえ、必要な場合には、'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(20,32)',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf()',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  ]
  return isOpen ? (
    <div>
      <div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
          <div style={titleStyle}>
            <IconFile style={{ marginRight: '8px' }} />
            参照リスト
          </div>
          <div onClick={toggleOpen} style={titleStyle}>
            {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
          </div>
        </div>
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>

          <ul>
            {itemsample.map((item, index) => (
              <a href={`${item.source}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
                <li
                  style={listItemStyle(index === hoveredItemId)}
                  onMouseEnter={() => setHoveredItemId(index)}
                  onMouseLeave={() => setHoveredItemId(null)}
                >
                  <div style={listContentStyle}>
                    <div style={listTitleStyle}>{item.title}</div>
                    {/* PDF预览组件 */}
                    <div style={pdfPreviewStyle}>
                      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
                    </div>
                    {/* 内容部分 */}
                    <div style={contentStyle}>
                      <p>{item.content}</p>
                    </div>
                  </div>
                </li>
              </a>
            ))}
          </ul>
        </div>
      </div>
    </div>
  ) : (
    <OpenSidebarButton onClick={toggleOpen} side={side} />
  );
};

export default ReferenceSidebar;
kirin-ri commented 5 months ago
const listItemStyle = (isHovered: boolean) => ({
  display: 'flex',
  flexDirection: 'row', // 修改为横向布局
  alignItems: 'flex-start', // 确保子元素从顶部开始对齐
  marginBottom: '10px',
  border: '1px solid white',
  padding: '20px',
  borderRadius: '5px',
  backgroundColor: isHovered ? '#0056b3' : '#343541',
  transition: 'background-color 0.3s',
  maxWidth: '350px',
  overflow: 'hidden', // 防止内容超出
});

const listContentStyle = {
  display: 'flex', 
  flexDirection: 'column', // 修改为纵向布局
  alignItems: 'flex-start', // 从顶部开始对齐
  width: 'calc(100% - 100px)', // 留出空间给PDF预览
};

const pdfPreviewContainerStyle = {
  width: '100px', // 为PDF预览分配固定宽度
  height: '140px', // 分配高度,可以根据需求调整
  display: 'flex',
  alignItems: 'center', // 居中显示
  justifyContent: 'center', // 居中显示
  overflow: 'hidden', // 防止内容超出
};

// 内容部分样式调整
const contentStyle = {
  display: '-webkit-box',
  WebkitBoxOrient: 'vertical',
  WebkitLineClamp: 3, // 限制在3行内显示省略号
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  width: '100%', // 确保占满剩余空间
};

// 在JSX中应用PDF预览容器样式
// ...

<li
  style={listItemStyle(index === hoveredItemId)}
  onMouseEnter={() => setHoveredItemId(index)}
  onMouseLeave={() => setHoveredItemId(null)}
>
  <div style={listContentStyle}>
    {/* PDF预览组件放在外层,旁边是标题和内容 */}
    <div style={pdfPreviewContainerStyle}>
      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)} style={pdfPreviewStyle}/>
    </div>
    <div>
      <div style={listTitleStyle}>{item.title}</div>
      <div style={contentStyle}>
        <p>{item.content}</p>
      </div>
    </div>
  </div>
</li>

// ...
kirin-ri commented 5 months ago

Type '{ fileUrl: string; pageNumber: number; style: { width: string; height: string; objectFit: string; }; }' is not assignable to type 'IntrinsicAttributes & PDFPreviewProps'. Property 'style' does not exist on type 'IntrinsicAttributes & PDFPreviewProps'

kirin-ri commented 5 months ago
const listItemStyle = (isHovered: boolean) => ({
  display: 'flex',
  flexDirection: 'column', // 内容垂直排列
  marginBottom: '10px',
  border: '1px solid white',
  padding: '20px',
  borderRadius: '5px',
  backgroundColor: isHovered ? '#0056b3' : '#343541',
  transition: 'background-color 0.3s',
  maxWidth: '350px',
  overflow: 'hidden', // 防止内容超出
});

const titleAndContentContainerStyle = {
  display: 'flex',
  flexDirection: 'row', // 标题和内容水平排列
  alignItems: 'flex-start', // 从顶部对齐
};

const pdfPreviewStyle = {
  width: '100px', // 为PDF预览分配固定宽度
  height: '140px', // 分配高度
  marginRight: '20px', // 与内容之间的间隔
};

const contentStyle = {
  display: '-webkit-box',
  WebkitBoxOrient: 'vertical',
  WebkitLineClamp: 3, // 限制在3行内显示省略号
  overflow: 'hidden',
  textOverflow: 'ellipsis',
};

// 在JSX中应用样式
// ...

<li
  style={listItemStyle(index === hoveredItemId)}
  onMouseEnter={() => setHoveredItemId(index)}
  onMouseLeave={() => setHoveredItemId(null)}
>
  <div style={listTitleStyle}>{item.title}</div> {/* 标题 */}
  <div style={titleAndContentContainerStyle}> {/* PDF预览和内容的容器 */}
    {/* PDF预览组件 */}
    <div style={pdfPreviewStyle}>
      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
    </div>
    {/* 内容部分 */}
    <div style={contentStyle}>
      <p>{item.content}</p>
    </div>
  </div>
</li>

// ...
kirin-ri commented 5 months ago
import React, { useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

    const loadPdf = async () => {
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        // 使用传入的pageNumber获取指定的PDF页面
        const page = await pdf.getPage(pageNumber);
        const scale = 0.15; // 调整缩放比例以适应预览大小
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]); // 添加pageNumber作为依赖项

  return <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;
kirin-ri commented 5 months ago
import React, { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  // 新增状态isLoading来表示PDF是否正在加载
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

    const loadPdf = async () => {
      setIsLoading(true); // 开始加载时设置isLoading为true
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        const page = await pdf.getPage(pageNumber);
        const scale = 0.15;
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      } finally {
        setIsLoading(false); // 加载完成或失败后设置isLoading为false
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]);

  // 在PDF正在加载时显示加载提示
  if (isLoading) {
    return <div>Loading PDF...</div>;
  }

  // PDF加载完成后显示canvas
  return <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;
kirin-ri commented 5 months ago
import React, { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  // 使用一个状态来追踪PDF是否已经加载和渲染完成
  const [isPdfLoaded, setIsPdfLoaded] = useState(false);

  useEffect(() => {
    pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

    const loadPdf = async () => {
      setIsPdfLoaded(false); // 开始加载前设置为false
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        const page = await pdf.getPage(pageNumber);
        const scale = 1; // 根据需要调整缩放比例
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;
            setIsPdfLoaded(true); // 渲染完成后设置为true
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]);

  // 只有当PDF加载和渲染完成后,才显示canvas
  return isPdfLoaded ? <canvas ref={canvasRef}></canvas> : null;
};

export default PDFPreview;
kirin-ri commented 5 months ago
import { IconFolderPlus, IconMistOff, IconPlus, IconFile } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import { Reference } from '@/types/reference';
import {
  CloseSidebarButton,
  OpenSidebarButton,
} from './components/OpenCloseButton';
import PDFPreview from '../Referencebar/PDFPreview';

// interface Reference {
//   id: string;
//   title: string;
//   description: string;
// }

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({
  isOpen,
  side,
  items,
  toggleOpen,
}: Props<T>) => {
  items.map((item,index) =>{
    console.log(item)
  })
  const { t } = useTranslation('promptbar');
  const [hoveredItemId, setHoveredItemId] = useState<number | null>(null);

  const allowDrop = (e: any) => {
    e.preventDefault();
  };

  const highlightDrop = (e: any) => {
    e.target.style.background = '#343541';
  };

  const removeHighlight = (e: any) => {
    e.target.style.background = 'none';
  };

  const titleStyle = {
    display: 'flex',
    alignItems: 'center',
    marginBottom: '30px',
    fontSize:'18px'
  }
  const headerStyle = {
    display: 'flex',
    justiftContent: 'space-between',
    alignItems:'center',
    fontSize: '15px',
    marginBottom: '15px',
    width: '100%',
  }
  const titleIcon = {
    display: 'flex',
    marginRight: '8px',
  }

  const listTitleStyle = {
    marginBottom: '10px',
    width: '100',
  }
  const listItemStyle = (isHovered: boolean) => ({
    display: 'flex',
    flexDirection: 'column' as 'column', // 内容垂直排列
    marginBottom: '10px',
    border: '1px solid white',
    padding: '20px',
    borderRadius: '5px',
    backgroundColor: isHovered ? '#0056b3' : '#343541',
    transition: 'background-color 0.3s',
    maxWidth: '350px',
    overflow: 'hidden', // 防止内容超出
  });

  const titleAndContentContainerStyle = {
    display: 'flex',
    flexDirection: 'row' as 'row', // 标题和内容水平排列
    alignItems: 'flex-start', // 从顶部对齐
  };

  const contentStyle = {
    display: '-webkit-box',
    WebkitBoxOrient: 'vertical' as 'vertical',
    WebkitLineClamp: 7, // 限制在3行内显示省略号
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  };

  // PDF预览组件的样式
  const pdfPreviewStyle = {
    width: '100px', // 为PDF预览分配固定宽度
    height: '140px', // 分配高度
    marginRight: '20px', // 与内容之间的间隔
  };

  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? `#page=${match[1]}` : '';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? parseInt(match[1], 10) : 1;
  };  
  const itemsample = [
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(30)',content:'日の原子力規制委員会に提出された論点のうちの残84された論点(例えば、新設炉と既設炉で目標値を分けるべきか否かなど)に関する議論を含め、安全目標に関する議論は、継続的な安全性向上を目指す原子力規制委員会として、今後とも引き続き検討を進めていくものとする。(3)新規制基準と安全目標の関係について原子力規制委員会は、安全目標は、基準ではなく規制を進めていく上で達成を目指す目標であると位置付けた。そして、原子炉等規制法の改正により新設された43条の3の29(発電用原子炉施設の安全性の向上のための評価)により、発電用原子炉設置者は施設定期検査終了後6ヶ月以内に自ら、安全性の向上のための評価を実施し、その結果を原子力規制委員会に届け出ることとなる。この安全性向上のための評価には、炉心損傷頻度、格納容器機能喪失頻度及びセシウム137の放出量が100テラベクレルを超えるような事故の発生頻度の評価が含まれており、原子力規制委員会は安全目標を参考にこの評価結果を踏まえ、必要な場合には、'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(20,32)',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
    {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf()',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  ]
  return isOpen ? (
    <div>
      <div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
          <div style={titleStyle}>
            <IconFile style={{ marginRight: '8px' }} />
            参照リスト
          </div>
          <div onClick={toggleOpen} style={titleStyle}>
            {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
          </div>
        </div>
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>

          <ul>
            {itemsample.map((item, index) => (
              <a href={`${item.source}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
                <li
                  style={listItemStyle(index === hoveredItemId)}
                  onMouseEnter={() => setHoveredItemId(index)}
                  onMouseLeave={() => setHoveredItemId(null)}
                >
                  <div style={listTitleStyle}>{item.title}</div> {/* 标题 */}
                  <div style={titleAndContentContainerStyle}> {/* PDF预览和内容的容器 */}
                    {/* PDF预览组件 */}
                    <div style={pdfPreviewStyle}>
                      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
                    </div>
                    {/* 内容部分 */}
                    <div style={contentStyle}>
                      <p>{item.content}</p>
                    </div>
                  </div>
                </li>
              </a>
            ))}
          </ul>
        </div>
      </div>
    </div>
  ) : (
    <OpenSidebarButton onClick={toggleOpen} side={side} />
  );
};

export default ReferenceSidebar;
kirin-ri commented 5 months ago

w-full cursor-pointer bg-transparent p-2 text-neutral-700 dark:text-neutral-200

kirin-ri commented 5 months ago

className="flex w-full cursor-pointer select-none items-center gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10 "

kirin-ri commented 5 months ago
import { IconClearAll, IconSettings } from '@tabler/icons-react';
import {
  MutableRefObject,
  memo,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from 'react';
import toast from 'react-hot-toast';

import { useTranslation } from 'next-i18next';

import { getEndpoint } from '@/utils/app/api';
import {
  saveConversation,
  saveConversations,
  updateConversation,
} from '@/utils/app/conversation';
import { throttle } from '@/utils/data/throttle';

import { ChatBody, Conversation, Message } from '@/types/chat';
import { Plugin } from '@/types/plugin';

import HomeContext from '@/pages/api/home/home.context';

import Spinner from '../Spinner';
import { ChatInput } from './ChatInput';
import { ChatLoader } from './ChatLoader';
import { ErrorMessageDiv } from './ErrorMessageDiv';
import { ModelSelect } from './ModelSelect';
import { SystemPrompt } from './SystemPrompt';
import { TemperatureSlider } from './Temperature';
import { MemoizedChatMessage } from './MemoizedChatMessage';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { KB_USER_PROMPT, KB_SYSTEM_PROMPT, AZURE_COGNITIVE_SEARCH_INDEX, KB_QUERY_SYSTEM_ROLE } from '@/utils/app/const';
import { Reference } from '@/types/reference'; 

interface Props {
  stopConversationRef: MutableRefObject<boolean>;
}

function getLocalStorage<S>(key: string, defaultValue: S) : S {
  const jsonValue = localStorage.getItem(key);
  if (jsonValue !== null) return JSON.parse(jsonValue);
  return defaultValue;
}

export const Chat = memo(({ stopConversationRef }: Props) => {
  const { t } = useTranslation('chat');

  const {
    state: {
      selectedConversation,
      conversations,
      models,
      apiKey,
      pluginKeys,
      serverSideApiKeyIsSet,
      messageIsStreaming,
      modelError,
      loading,
      prompts,
      references,
    },
    handleUpdateConversation,
    dispatch: homeDispatch,
  } = useContext(HomeContext);

  const [currentMessage, setCurrentMessage] = useState<Message>();
  const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true);
  const [showSettings, setShowSettings] = useState<boolean>(false);
  const [showScrollDownButton, setShowScrollDownButton] =
    useState<boolean>(false);
  const [plugin, setPlugin] = useState<Plugin | null>(null);

  const messagesEndRef = useRef<HTMLDivElement>(null);
  const chatContainerRef = useRef<HTMLDivElement>(null);
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const handlePluginChange = ( p: Plugin ) => {
    setPlugin( p );
  }

  const handleSend = useCallback(
    async (message: Message, deleteCount = 0, plugin: Plugin | null) => {
      if (selectedConversation) {
        let updatedConversation: Conversation;
        if (deleteCount) {
          const updatedMessages = [...selectedConversation.messages];
          for (let i = 0; i < deleteCount; i++) {
            updatedMessages.pop();
          }
          updatedConversation = {
            ...selectedConversation,
            messages: [...updatedMessages, message],
          };
        } else {
          updatedConversation = {
            ...selectedConversation,
            messages: [...selectedConversation.messages, message],
          };
        }
        homeDispatch({
          field: 'selectedConversation',
          value: updatedConversation,
        });
        homeDispatch({ field: 'loading', value: true });
        homeDispatch({ field: 'messageIsStreaming', value: true });
        const chatBody: ChatBody = {
          model: updatedConversation.model,
          messages: updatedConversation.messages,
          key: apiKey,
          prompt: updatedConversation.prompt,
          temperature: updatedConversation.temperature,
        };
        const endpoint = getEndpoint(plugin);
        let body;
        if (!plugin) {
          body = JSON.stringify(chatBody);
        } else if (plugin.id === 'qdrant-search') {
          const kbUserPrompt = getLocalStorage('kbUserPrompt', KB_USER_PROMPT );
          const kbSystemPrompt = getLocalStorage('kbSystemPrompt', KB_SYSTEM_PROMPT);
          const kbCogSearchIndex = getLocalStorage('kbCogSearchIndex', AZURE_COGNITIVE_SEARCH_INDEX.split(',')[0]);
          const kbSearchSystemPrompt = getLocalStorage('kbSearchSystemPrompt', KB_QUERY_SYSTEM_ROLE);
          const kbDebugMode = getLocalStorage('kbDebugMode', false);
          const kbSearchQueryEnabled = getLocalStorage('kbSearchQueryEnabled', true);

          body = JSON.stringify({
            ...chatBody,
            kbSearchQueryEnabled: kbSearchQueryEnabled,
            kbSearchSystemPrompt: kbSearchSystemPrompt,
            kbUserPrompt: kbUserPrompt,
            kbSystemPrompt: kbSystemPrompt,
            kbCogSearchIndex: kbCogSearchIndex,
            kbDebugMode: kbDebugMode
          });
        } else if (plugin.id === 'google-search') {
          body = JSON.stringify({
            ...chatBody,
            googleAPIKey: pluginKeys
              .find((key) => key.pluginId === 'google-search')
              ?.requiredKeys.find((key) => key.key === 'GOOGLE_API_KEY')?.value,
            googleCSEId: pluginKeys
              .find((key) => key.pluginId === 'google-search')
              ?.requiredKeys.find((key) => key.key === 'GOOGLE_CSE_ID')?.value,
          });
        }
        const controller = new AbortController();
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          signal: controller.signal,
          body,
        });
        if (!response.ok) {
          homeDispatch({ field: 'loading', value: false });
          homeDispatch({ field: 'messageIsStreaming', value: false });
          // toast.error(response.statusText);
          const { error } = await response.json();
          alert( error );
          return;
        }
        const data = response.body;
        if (!data) {
          homeDispatch({ field: 'loading', value: false });
          homeDispatch({ field: 'messageIsStreaming', value: false });
          return;
        }
        if (!plugin) {
          if (updatedConversation.messages.length === 1) {
            const { content } = message;
            const customName =
              content.length > 30 ? content.substring(0, 30) + '...' : content;
            updatedConversation = {
              ...updatedConversation,
              name: customName,
            };
          }
          homeDispatch({ field: 'loading', value: false });
          const reader = data.getReader();
          const decoder = new TextDecoder();
          let done = false;
          let isFirst = true;
          let text = '';
          while (!done) {
            if (stopConversationRef.current === true) {
              controller.abort();
              done = true;
              break;
            }
            const { value, done: doneReading } = await reader.read();
            done = doneReading;
            const chunkValue = decoder.decode(value);
            text += chunkValue;
            if (isFirst) {
              isFirst = false;
              const updatedMessages: Message[] = [
                ...updatedConversation.messages,
                { role: 'assistant', content: chunkValue },
              ];
              updatedConversation = {
                ...updatedConversation,
                messages: updatedMessages,
              };
              homeDispatch({
                field: 'selectedConversation',
                value: updatedConversation,
              });
            } else {
              const updatedMessages: Message[] =
                updatedConversation.messages.map((message, index) => {
                  if (index === updatedConversation.messages.length - 1) {
                    return {
                      ...message,
                      content: text,
                    };
                  }
                  return message;
                });
              updatedConversation = {
                ...updatedConversation,
                messages: updatedMessages,
              };
              homeDispatch({
                field: 'selectedConversation',
                value: updatedConversation,
              });
            }
          }
          saveConversation(updatedConversation);
          const updatedConversations: Conversation[] = conversations.map(
            (conversation) => {
              if (conversation.id === selectedConversation.id) {
                return updatedConversation;
              }
              return conversation;
            },
          );
          if (updatedConversations.length === 0) {
            updatedConversations.push(updatedConversation);
          }
          homeDispatch({ field: 'conversations', value: updatedConversations });
          saveConversations(updatedConversations);
          homeDispatch({ field: 'messageIsStreaming', value: false });
        } else {
          const answer  = await response.json();
          console.log(answer.test.a)
          homeDispatch({field :'references', value:answer.test.a})
          const updatedMessages: Message[] = [
            ...updatedConversation.messages,
            { role: 'assistant', content: answer.test.Answer },
          ];
          if (updatedConversation.messages.length === 1) {
            const { content } = message;
            const customName =
              content.length > 30 ? content.substring(0, 30) + '...' : content;
            updatedConversation = {
              ...updatedConversation,
              name: customName,
            };
          }
          updatedConversation = {
            ...updatedConversation,
            messages: updatedMessages,
          };
          homeDispatch({
            field: 'selectedConversation',
            value: updatedConversation,
          });
          saveConversation(updatedConversation);
          const updatedConversations: Conversation[] = conversations.map(
            (conversation) => {
              if (conversation.id === selectedConversation.id) {
                return updatedConversation;
              }
              return conversation;
            },
          );
          if (updatedConversations.length === 0) {
            updatedConversations.push(updatedConversation);
          }
          homeDispatch({ field: 'conversations', value: updatedConversations });
          saveConversations(updatedConversations);
          homeDispatch({ field: 'loading', value: false });
          homeDispatch({ field: 'messageIsStreaming', value: false });
        }
      }
    },
    [
      apiKey,
      conversations,
      pluginKeys,
      selectedConversation,
      stopConversationRef,
    ],
  );

  const scrollToBottom = useCallback(() => {
    if (autoScrollEnabled) {
      messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
      textareaRef.current?.focus();
    }
  }, [autoScrollEnabled]);

  const handleScroll = () => {
    if (chatContainerRef.current) {
      const { scrollTop, scrollHeight, clientHeight } =
        chatContainerRef.current;
      const bottomTolerance = 30;

      if (scrollTop + clientHeight < scrollHeight - bottomTolerance) {
        setAutoScrollEnabled(false);
        setShowScrollDownButton(true);
      } else {
        setAutoScrollEnabled(true);
        setShowScrollDownButton(false);
      }
    }
  };

  const handleScrollDown = () => {
    chatContainerRef.current?.scrollTo({
      top: chatContainerRef.current.scrollHeight,
      behavior: 'smooth',
    });
  };

  const handleSettings = () => {
    setShowSettings(!showSettings);
  };

  const onClearAll = () => {
    if (
      confirm(t<string>('Are you sure you want to clear all messages?')) &&
      selectedConversation
    ) {
      handleUpdateConversation(selectedConversation, {
        key: 'messages',
        value: [],
      });
    }
  };

  const scrollDown = () => {
    if (autoScrollEnabled) {
      messagesEndRef.current?.scrollIntoView(true);
    }
  };
  const throttledScrollDown = throttle(scrollDown, 250);

  // useEffect(() => {
  //   console.log('currentMessage', currentMessage);
  //   if (currentMessage) {
  //     handleSend(currentMessage);
  //     homeDispatch({ field: 'currentMessage', value: undefined });
  //   }
  // }, [currentMessage]);

  useEffect(() => {
    throttledScrollDown();
    selectedConversation &&
      setCurrentMessage(
        selectedConversation.messages[selectedConversation.messages.length - 2],
      );
  }, [selectedConversation, throttledScrollDown]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setAutoScrollEnabled(entry.isIntersecting);
        if (entry.isIntersecting) {
          textareaRef.current?.focus();
        }
      },
      {
        root: null,
        threshold: 0.5,
      },
    );
    const messagesEndElement = messagesEndRef.current;
    if (messagesEndElement) {
      observer.observe(messagesEndElement);
    }
    return () => {
      if (messagesEndElement) {
        observer.unobserve(messagesEndElement);
      }
    };
  }, [messagesEndRef]);

  return (
    <div className="relative flex-1 overflow-hidden bg-white dark:bg-[#343541]">
      {!(apiKey || serverSideApiKeyIsSet) ? (
        <div className="mx-auto flex h-full w-[300px] flex-col justify-center space-y-6 sm:w-[600px]">
          <div className="text-center text-4xl font-bold text-black dark:text-white">
            Welcome to Chatbot UI
          </div>
          <div className="text-center text-lg text-black dark:text-white">
            <div className="mb-8">{`Chatbot UI is an open source clone of OpenAI's ChatGPT UI.`}</div>
            <div className="mb-2 font-bold">
              Important: Chatbot UI is 100% unaffiliated with OpenAI.
            </div>
          </div>
          <div className="text-center text-gray-500 dark:text-gray-400">
            <div className="mb-2">
              Chatbot UI allows you to plug in your API key to use this UI with
              their API.
            </div>
            <div className="mb-2">
              It is <span className="italic">only</span> used to communicate
              with their API.
            </div>
            <div className="mb-2">
              {t(
                'Please set your OpenAI API key in the bottom left of the sidebar.',
              )}
            </div>
            <div>
              {t("If you don't have an OpenAI API key, you can get one here: ")}
              <a
                href="https://platform.openai.com/account/api-keys"
                target="_blank"
                rel="noreferrer"
                className="text-blue-500 hover:underline"
              >
                openai.com
              </a>
            </div>
          </div>
        </div>
      ) : modelError ? (
        <ErrorMessageDiv error={modelError} />
      ) : (
        <>
          <div
            className="max-h-full overflow-x-hidden"
            ref={chatContainerRef}
            onScroll={handleScroll}
          >
            {selectedConversation?.messages.length === 0 ? (
              <>
                <div className="mx-auto flex flex-col space-y-5 md:space-y-10 px-3 pt-5 md:pt-12 sm:max-w-[600px]">
                  <div className="text-center text-3xl font-semibold text-gray-800 dark:text-gray-100">
                    {models.length === 0 ? (
                      <div>
                        <Spinner size="16px" className="mx-auto" />
                      </div>
                    ) : (
                      'Chatbot UI'
                    )}
                  </div>

                  {models.length > 0 && (
                    <div className="flex h-full flex-col space-y-4 rounded-lg border border-neutral-200 p-4 dark:border-neutral-600">
                      <ModelSelect />

                      <SystemPrompt
                        conversation={selectedConversation}
                        prompts={prompts}
                        onChangePrompt={(prompt) =>
                          handleUpdateConversation(selectedConversation, {
                            key: 'prompt',
                            value: prompt,
                          })
                        }
                      />

                      <TemperatureSlider
                        label={t('Temperature')}
                        onChangeTemperature={(temperature) =>
                          handleUpdateConversation(selectedConversation, {
                            key: 'temperature',
                            value: temperature,
                          })
                        }
                      />
                    </div>
                  )}
                </div>
              </>
            ) : (
              <>
                <div className="sticky top-0 z-10 flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200">
                  {t('Model')}: {selectedConversation?.model.name} | {t('Temp')}
                  : {selectedConversation?.temperature} |
                  <button
                    className="ml-2 cursor-pointer hover:opacity-50"
                    onClick={handleSettings}
                  >
                    <IconSettings size={18} />
                  </button>
                  <button
                    className="ml-2 cursor-pointer hover:opacity-50"
                    onClick={onClearAll}
                  >
                    <IconClearAll size={18} />
                  </button>
                </div>
                {showSettings && (
                  <div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
                    <div className="flex h-full flex-col space-y-4 border-b border-neutral-200 p-4 dark:border-neutral-600 md:rounded-lg md:border">
                      <ModelSelect />
                    </div>
                  </div>
                )}

                {selectedConversation?.messages.map((message, index) => (
                  <MemoizedChatMessage
                    key={index}
                    message={message}
                    messageIndex={index}
                    onEdit={(editedMessage) => {
                      setCurrentMessage(editedMessage);
                      // discard edited message and the ones that come after then resend
                      handleSend(
                        editedMessage,
                        selectedConversation?.messages.length - index,
                        plugin
                      );
                    }}
                  />
                ))}

                {loading && <ChatLoader />}

                <div
                  className="h-[162px] bg-white dark:bg-[#343541]"
                  ref={messagesEndRef}
                />
              </>
            )}
          </div>

          <ChatInput
            stopConversationRef={stopConversationRef}
            textareaRef={textareaRef}
            onSend={(message, plugin) => {
              setCurrentMessage(message);
              handleSend(message, 0, plugin);
            }}
            onScrollDownClick={handleScrollDown}
            onRegenerate={() => {
              if (currentMessage) {
                handleSend(currentMessage, 2, plugin);
              }
            }}
            showScrollDownButton={showScrollDownButton}
            plugin={plugin}
            handlePluginChange={handlePluginChange}
          />
        </>
      )}
    </div>
  );
});
Chat.displayName = 'Chat';
kirin-ri commented 5 months ago
import { IconFile } from '@tabler/icons-react';
import { useState } from 'react';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import { Reference } from '@/types/reference';
import {
  OpenSidebarButton,
} from './components/OpenCloseButton';
import PDFPreview from '../Referencebar/PDFPreview';

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({
  isOpen,
  side,
  items,
  toggleOpen,
}: Props<T>) => {
  items.map((item,index) =>{
    console.log(item)
  })
  const [hoveredItemId, setHoveredItemId] = useState<number | null>(null);

  const titleStyle = {
    display: 'flex',
    alignItems: 'center',
    marginBottom: '20px',
    fontSize:'18px'
  }

  const listTitleStyle = {
    marginBottom: '10px',
    width: '100',
  }
  const listItemStyle = (isHovered: boolean) => ({
    display: 'flex',
    flexDirection: 'column' as 'column', 
    marginBottom: '10px',
    border: '1px solid white',
    padding: '20px',
    borderRadius: '5px',
    backgroundColor: isHovered ? '#0056b3' : '#343541',
    transition: 'background-color 0.3s',
    maxWidth: '400px',
    width: '100%',
    overflow: 'hidden', 
  });

  const titleAndContentContainerStyle = {
    display: 'flex',
    flexDirection: 'row' as 'row', 
    alignItems: 'flex-start', 
  };

  const contentStyle = {
    display: '-webkit-box',
    WebkitBoxOrient: 'vertical' as 'vertical',
    WebkitLineClamp: 7, 
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  };

  const pdfPreviewStyle = {
    width: '100px', 
    height: '140px', 
    marginRight: '20px', 
  };

  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? `#page=${match[1]}` : '';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? parseInt(match[1], 10) : 1;
  };  
  // const itemsample = [
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(30)',content:'日の原子力規制委員会に提出された論点のうちの残84された論点(例えば、新設炉と既設炉で目標値を分けるべきか否かなど)に関する議論を含め、安全目標に関する議論は、継続的な安全性向上を目指す原子力規制委員会として、今後とも引き続き検討を進めていくものとする。(3)新規制基準と安全目標の関係について原子力規制委員会は、安全目標は、基準ではなく規制を進めていく上で達成を目指す目標であると位置付けた。そして、原子炉等規制法の改正により新設された43条の3の29(発電用原子炉施設の安全性の向上のための評価)により、発電用原子炉設置者は施設定期検査終了後6ヶ月以内に自ら、安全性の向上のための評価を実施し、その結果を原子力規制委員会に届け出ることとなる。この安全性向上のための評価には、炉心損傷頻度、格納容器機能喪失頻度及びセシウム137の放出量が100テラベクレルを超えるような事故の発生頻度の評価が含まれており、原子力規制委員会は安全目標を参考にこの評価結果を踏まえ、必要な場合には、'},
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(20,32)',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf()',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  // ]
  return isOpen ? (
    <div>
      <div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
          <div style={titleStyle}>
            <IconFile style={{ marginRight: '8px' }} />
            参照リスト
          </div>
          <div onClick={toggleOpen} style={titleStyle}>
            {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
          </div>
        </div>
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>
          <ul>
            {items.map((item, index) => (
              <a href={`${item.source}?v=${new Date().getTime}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
                <li
                  style={listItemStyle(index === hoveredItemId)}
                  onMouseEnter={() => setHoveredItemId(index)}
                  onMouseLeave={() => setHoveredItemId(null)}
                >
                  <div style={listTitleStyle}>{item.title}</div>
                  <div style={titleAndContentContainerStyle}>
                    <div style={pdfPreviewStyle}>
                      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
                    </div>
                    <div style={contentStyle}>
                      <p>{item.content}</p>
                    </div>
                  </div>
                </li>
              </a>
            ))}
          </ul>
        </div>
      </div>
    </div>
  ) : (
    <OpenSidebarButton onClick={toggleOpen} side={side} />
  );
};

export default ReferenceSidebar;
kirin-ri commented 5 months ago
import { useEffect } from 'react';
// その他のimport宣言...

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({ isOpen, side, items, toggleOpen }: Props<T>) => {
  useEffect(() => {
    // URLからクエリパラメータを取得
    const queryParams = new URLSearchParams(window.location.search);
    const page = queryParams.get('page');

    // page=1 の場合のみ、localStorageをクリア
    if (page === '1') {
      localStorage.clear();
      console.log('localStorageがクリアされました。');
    }
  }, []); // 空の依存配列を指定して、コンポーネントのマウント時にのみ実行

  // コンポーネントの残りの部分...
};
kirin-ri commented 5 months ago
const ReferenceSidebar = <T,>({ isOpen, side, items, toggleOpen }: Props<T>) => {

  // リンククリック時のハンドラー
  const handleLinkClick = (pageNumber: number, event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
    if (pageNumber === 1) {
      localStorage.clear();
      console.log('localStorageがクリアされました。ページ番号が1です。');
    }
  };

  return isOpen ? (
    <div>
      {/* 省略 */}
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>
          <ul>
            {items.map((item, index) => (
              <a 
                href={`${item.source}?v=${new Date().getTime()}${getPageNumberFromTitle(item.title)}`} 
                target="_blank" 
                rel="noopener noreferrer"
                onClick={(e) => handleLinkClick(extractPageNumberFromTitle(item.title), e)}
              >
                <li
                  style={listItemStyle(index === hoveredItemId)}
                  onMouseEnter={() => setHoveredItemId(index)}
                  onMouseLeave={() => setHoveredItemId(null)}
                >
                  {/* リストアイテムの内容 */}
kirin-ri commented 5 months ago

PDF表示とハイライトの技術実現可能性について 概要 本技術は、ReactとTypeScriptをベースに、PDF.jsライブラリを使用してPDFファイルをウェブページに表示し、特定のテキスト部分をハイライトする機能を実装します。 新しいタブでPDFを開き、ユーザーが指定したテキストをハイライトすることが主な目的です。 実装ステップ PDF.jsの導入:PDFファイルをウェブページ上でレンダリングし、操作するためにPDF.jsライブラリを使用します。npm経由でライブラリをプロジェクトに追加します。

Reactコンポーネントの作成:PDFファイルの読み込み、表示、および特定部分のハイライトを担当するReactコンポーネントを作成します。

新しいタブでのPDF表示:ユーザーがリンクをクリックすると、新しいタブが開き、指定されたPDFファイルが表示されます。ハイライトするテキスト情報は、URLパラメータ、localStorage、またはsessionStorageを通じて新しいタブに渡すことができます。

技術的考慮事項 跨标签通信(クロスタブ通信):オリジナルのタブとPDFが表示される新しいタブとの間で情報を同期する必要がある場合、localStorageやsessionStorageなどのブラウザストレージを利用する方法があります。 セキュリティ:異なるオリジンからのPDFファイルを扱う場合は、CORS(クロスオリジンリソース共有)に関する問題に注意してください。 パフォーマンスとリソース管理:PDFの処理はリソースを大量に消費する可能性があるため、コンポーネントのライフサイクルとリソースを適切に管理することが重要です。 結論 ReactとTypeScript、およびPDF.jsを用いたPDFの表示とテキストのハイライトは技術的に実現可能です。ただし、実装には上記の技術的考慮事項に留意し、プロジェクトの具体的な要件に合わせたカスタマイズが必要になります。この技術を利用することで、ユーザーに対してよりリッチな文書閲覧体験を提供することが可能です。

kirin-ri commented 5 months ago

react-pdf と Canvas 描画を使用して PDF 文書の特定の内容をハイライトする方法について、日本語で説明します。

ステップ 1: PDF ページのレンダリング まず、react-pdf の Page コンポーネントを使用して、ハイライトしたい PDF ページをレンダリングします。Page コンポーネントは canvasRef 属性を提供しており、これを使用して PDF をレンダリングする Canvas 要素の参照を取得できます。

ステップ 2: Canvas API を使用してハイライトを追加 Canvas の参照を取得した後、Canvas API を使用してその上にハイライト領域を描画します。これには通常、fillRect メソッドを使用して矩形を描画するか、もしくはもっと複雑な形状のハイライトが必要な場合は beginPath、moveTo、lineTo などのメソッドを使用します。

kirin-ri commented 5 months ago
import React, { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [imageSrc, setImageSrc] = useState<string | null>(null);

  useEffect(() => {
    const loadPdf = async () => {
      if (imageSrc) return; // 如果已有图片,则不重新渲染PDF

      pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        const page = await pdf.getPage(pageNumber);
        const scale = 0.5;
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;

            // 将canvas内容转换为图片URL
            const imageDataUrl = canvas.toDataURL();
            setImageSrc(imageDataUrl); // 保存图片URL以便后续使用
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber, imageSrc]); // 添加imageSrc作为依赖项

  // 如果有图片URL,直接展示图片,否则展示canvas
  return imageSrc ? <img src={imageSrc} alt={`Page ${pageNumber}`} /> : <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;
kirin-ri commented 5 months ago

return imageSrc ? <img src={imageSrc} alt={Page ${pageNumber}} style={{ width: canvasRef.current?.width, height: canvasRef.current?.height }} /> : <canvas ref={canvasRef}></canvas>;

kirin-ri commented 5 months ago

return imageSrc ? <img src={imageSrc} alt={Page ${pageNumber}} style={{ width: canvasRef.current?.width, height: canvasRef.current?.height }} /> : <canvas ref={canvasRef}></canvas>;

kirin-ri commented 5 months ago

return imageSrc ? <img src={imageSrc} alt={Page ${pageNumber}} style={{ width: canvasRef.current?.width, height: canvasRef.current?.height }} /> : <canvas ref={canvasRef}></canvas>;

kirin-ri commented 5 months ago

return imageSrc ? <img src={imageSrc} alt={Page ${pageNumber}} style={{ width: canvasRef.current?.width, height: canvasRef.current?.height }} /> : ;

kirin-ri commented 5 months ago

: ;

kirin-ri commented 5 months ago

: <canvas ref={canvasRef}></canvas>;

kirin-ri commented 5 months ago

return ( imageSrc ? <img src={imageSrc} alt={Page ${pageNumber}} style={{ width:${canvasRef.current?.width}px, height:${canvasRef.current?.height}px` }} /> :

); `

kirin-ri commented 5 months ago
return (
  imageSrc ? 
  <img 
    src={imageSrc} 
    alt={`Page ${pageNumber}`} 
    style={{ 
      width: `${canvasRef.current?.width}px`, 
      height: `${canvasRef.current?.height}px`
    }} 
  /> : 
  <canvas ref={canvasRef}></canvas>
);
kirin-ri commented 5 months ago
import { IconFile } from '@tabler/icons-react';
import { useState } from 'react';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import { Reference } from '@/types/reference';
import {
  OpenSidebarButton,
} from './components/OpenCloseButton';
import PDFPreview from '../Referencebar/PDFPreview';

interface Props<T> {
  isOpen: boolean;
  side: 'left' | 'right';
  items: Reference[];
  toggleOpen: () => void;
}

const ReferenceSidebar = <T,>({
  isOpen,
  side,
  items,
  toggleOpen,
}: Props<T>) => {
  items.map((item,index) =>{
    console.log(item)
  })
  const [hoveredItemId, setHoveredItemId] = useState<number | null>(null);

  const titleStyle = {
    display: 'flex',
    alignItems: 'center',
    marginBottom: '20px',
    fontSize:'18px'
  }

  const listTitleStyle = {
    marginBottom: '10px',
    width: '100',
  }
  const listItemStyle = (isHovered: boolean) => ({
    display: 'flex',
    flexDirection: 'column' as 'column', 
    marginBottom: '10px',
    border: '1px solid white',
    padding: '20px',
    borderRadius: '5px',
    backgroundColor: isHovered ? '#0056b3' : '#343541',
    transition: 'background-color 0.3s',
    maxWidth: '400px',
    width: '100%',
    overflow: 'hidden', 
  });

  const titleAndContentContainerStyle = {
    display: 'flex',
    flexDirection: 'row' as 'row', 
    alignItems: 'flex-start', 
  };

  const contentStyle = {
    display: '-webkit-box',
    WebkitBoxOrient: 'vertical' as 'vertical',
    WebkitLineClamp: 7, 
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  };

  const pdfPreviewStyle = {
    width: '100px', 
    height: '140px', 
    marginRight: '20px', 
  };

  const getPageNumberFromTitle = (title: string) => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? `#page=${match[1]}` : '#page=1';
  };
  const extractPageNumberFromTitle = (title: string): number => {
    const match = title.match(/\((\d+)[,)]/);
    return match ? parseInt(match[1], 10) : 1;
  };  
  // const itemsample = [
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(30)',content:'日の原子力規制委員会に提出された論点のうちの残84された論点(例えば、新設炉と既設炉で目標値を分けるべきか否かなど)に関する議論を含め、安全目標に関する議論は、継続的な安全性向上を目指す原子力規制委員会として、今後とも引き続き検討を進めていくものとする。(3)新規制基準と安全目標の関係について原子力規制委員会は、安全目標は、基準ではなく規制を進めていく上で達成を目指す目標であると位置付けた。そして、原子炉等規制法の改正により新設された43条の3の29(発電用原子炉施設の安全性の向上のための評価)により、発電用原子炉設置者は施設定期検査終了後6ヶ月以内に自ら、安全性の向上のための評価を実施し、その結果を原子力規制委員会に届け出ることとなる。この安全性向上のための評価には、炉心損傷頻度、格納容器機能喪失頻度及びセシウム137の放出量が100テラベクレルを超えるような事故の発生頻度の評価が含まれており、原子力規制委員会は安全目標を参考にこの評価結果を踏まえ、必要な場合には、'},
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf(20,32)',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  //   {source:'https://caramlops-saas-0.westus.cloudapp.azure.com/file/test/index01/%E5%AE%9F%E7%94%A8%E7%99%BA%E9%9B%BB%E7%94%A8%E5%8E%9F%E5%AD%90%E7%82%89%E3%81%AB%E4%BF%82%E3%82%8B%E6%96%B0%E8%A6%8F%E5%88%B6%E5%9F%BA%E6%BA%96%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6.pdf',title:'実用発電原子炉に係る新規制基準の考え方について.pdf()',content:'規制委員会への届出及び要旨の公表が義務付けられている(同条3項)。また、内閣総理大臣及び原子力規制委員会は、原子力事業者防災業務計画が原子力災害の発生若しくは拡大を防止するために十分でないと認めるときは、原子力事業者に対し、上記計画の作成又は修正を命ずることができる(同条4項)。そして、原子力事業者が上記命令に違反した場合、原子力規制委員会は、発電用原子炉の設置許可の取消し又は1年以内の期間を定めてその運転の停止を命ずることができる。なお、原子力規制委員会は、届出のあった原子力事業者防災業務計画について、順次公表を行っている。77図1国による避難計画等の具体化・充実化支援等の全体図78§22-6安全目標と新規制基準の関係1相対的安全の考え方に立脚した原子力分野における安全目標本資料「§11-2」で述べたとおり、科学技術分野においては絶対的な安全性は達成することも要求することもできないものであるから、万が一、重大事故が発生したときは、当該原子炉施設の従業員やその周辺住民等の生命、身体に重大な危害を及'},
  // ]
  return isOpen ? (
    <div>
      <div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
          <div style={titleStyle}>
            <IconFile style={{ marginRight: '8px' }} />
            参照リスト
          </div>
          <div onClick={toggleOpen} style={titleStyle}>
            {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
          </div>
        </div>
        <div  style={{overflowY:'auto',flex:1,padding:'0 10px'}}>
          <ul>
            {items.map((item, index) => (
              <a href={`${item.source}?v=timestamp=${new Date().getTime()}${getPageNumberFromTitle(item.title)}`} target="_blank" rel="noopener noreferrer">
                <li
                  style={listItemStyle(index === hoveredItemId)}
                  onMouseEnter={() => setHoveredItemId(index)}
                  onMouseLeave={() => setHoveredItemId(null)}
                >
                  <div style={listTitleStyle}>{item.title}</div>
                  <div style={titleAndContentContainerStyle}>
                    <div style={pdfPreviewStyle}>
                      <PDFPreview fileUrl={item.source} pageNumber={extractPageNumberFromTitle(item.title)}/>
                    </div>
                    <div style={contentStyle}>
                      <p>{item.content}</p>
                    </div>
                  </div>
                </li>
              </a>
            ))}
          </ul>
        </div>
      </div>
    </div>
  ) : (
    <OpenSidebarButton onClick={toggleOpen} side={side} />
  );
};

export default ReferenceSidebar;
kirin-ri commented 5 months ago
import React, { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

const PDFPreview = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef(null);
  const [imageSrc, setImageSrc] = useState('');

  useEffect(() => {
    const loadPdf = async () => {
      pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        const page = await pdf.getPage(pageNumber);
        const viewport = page.getViewport({ scale: 1 });
        const canvas = canvasRef.current;
        if (canvas) {
          const context = canvas.getContext('2d');
          canvas.width = viewport.width;
          canvas.height = viewport.height;

          const renderContext = {
            canvasContext: context,
            viewport,
          };
          await page.render(renderContext).promise;

          const imageDataUrl = canvas.toDataURL();
          setImageSrc(imageDataUrl);
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber]);

  return (
    imageSrc ? 
      <img 
        src={imageSrc} 
        alt={`Page ${pageNumber}`} 
        style={{ 
          width: 'auto', 
          height: 'auto', 
          maxWidth: '100%', 
          maxHeight: '100vh' 
        }} 
      /> : 
      <canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
  );
};

export default PDFPreview;
kirin-ri commented 5 months ago
const pdfPreviewStyle = {
  width: '100px', // 控制预览图的宽度
  height: 'auto', // 高度自适应,保持宽高比
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center',
  overflow: 'hidden', // 避免内容溢出
  marginRight: '20px', 
};
kirin-ri commented 5 months ago
import React, { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import 'pdfjs-dist/legacy/build/pdf.worker.entry';

interface PDFPreviewProps {
  fileUrl: string;
  pageNumber: number;
}

const PDFPreview: React.FC<PDFPreviewProps> = ({ fileUrl, pageNumber }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [imageSrc, setImageSrc] = useState<string | null>(null);

  useEffect(() => {
    const loadPdf = async () => {
      if (imageSrc) return; // 如果已有图片,则不重新渲染PDF

      pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;

      try {
        const pdf = await pdfjsLib.getDocument(fileUrl).promise;
        const page = await pdf.getPage(pageNumber);
        const scale = 0.5;
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;

        if (canvas) {
          const context = canvas.getContext('2d');
          if (context) {
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport,
            };
            await page.render(renderContext).promise;

            // 将canvas内容转换为图片URL
            const imageDataUrl = canvas.toDataURL();
            setImageSrc(imageDataUrl); // 保存图片URL以便后续使用
          }
        }
      } catch (error) {
        console.error("Error loading PDF: ", error);
      }
    };

    loadPdf();
  }, [fileUrl, pageNumber, imageSrc]); // 添加imageSrc作为依赖项

  // 如果有图片URL,直接展示图片,否则展示canvas
  return imageSrc ? <img src={imageSrc} alt={`Page ${pageNumber}`} /> : <canvas ref={canvasRef}></canvas>;
};

export default PDFPreview;