Header.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { useEffect, useState } from 'react';
  2. import StorageUtils from '../utils/storage';
  3. import { useAppContext } from '../utils/app.context';
  4. import { classNames } from '../utils/misc';
  5. import daisyuiThemes from 'daisyui/src/theming/themes';
  6. import { THEMES } from '../Config';
  7. import { useNavigate } from 'react-router';
  8. import SettingDialog from './SettingDialog';
  9. export default function Header() {
  10. const navigate = useNavigate();
  11. const [selectedTheme, setSelectedTheme] = useState(StorageUtils.getTheme());
  12. const [showSettingDialog, setShowSettingDialog] = useState(false);
  13. const setTheme = (theme: string) => {
  14. StorageUtils.setTheme(theme);
  15. setSelectedTheme(theme);
  16. };
  17. useEffect(() => {
  18. document.body.setAttribute('data-theme', selectedTheme);
  19. document.body.setAttribute(
  20. 'data-color-scheme',
  21. // @ts-expect-error daisyuiThemes complains about index type, but it should work
  22. daisyuiThemes[selectedTheme]?.['color-scheme'] ?? 'auto'
  23. );
  24. }, [selectedTheme]);
  25. const { isGenerating, viewingConversation } = useAppContext();
  26. const isCurrConvGenerating = isGenerating(viewingConversation?.id ?? '');
  27. const removeConversation = () => {
  28. if (isCurrConvGenerating || !viewingConversation) return;
  29. const convId = viewingConversation.id;
  30. if (window.confirm('Are you sure to delete this conversation?')) {
  31. StorageUtils.remove(convId);
  32. navigate('/');
  33. }
  34. };
  35. const downloadConversation = () => {
  36. if (isCurrConvGenerating || !viewingConversation) return;
  37. const convId = viewingConversation.id;
  38. const conversationJson = JSON.stringify(viewingConversation, null, 2);
  39. const blob = new Blob([conversationJson], { type: 'application/json' });
  40. const url = URL.createObjectURL(blob);
  41. const a = document.createElement('a');
  42. a.href = url;
  43. a.download = `conversation_${convId}.json`;
  44. document.body.appendChild(a);
  45. a.click();
  46. document.body.removeChild(a);
  47. URL.revokeObjectURL(url);
  48. };
  49. return (
  50. <div className="flex flex-row items-center mt-6 mb-6">
  51. {/* open sidebar button */}
  52. <label htmlFor="toggle-drawer" className="btn btn-ghost lg:hidden">
  53. <svg
  54. xmlns="http://www.w3.org/2000/svg"
  55. width="16"
  56. height="16"
  57. fill="currentColor"
  58. className="bi bi-list"
  59. viewBox="0 0 16 16"
  60. >
  61. <path
  62. fillRule="evenodd"
  63. d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"
  64. />
  65. </svg>
  66. </label>
  67. <div className="grow text-2xl font-bold ml-2">llama.cpp</div>
  68. {/* action buttons (top right) */}
  69. <div className="flex items-center">
  70. <div v-if="messages.length > 0" className="dropdown dropdown-end">
  71. {/* "..." button */}
  72. <button
  73. tabIndex={0}
  74. role="button"
  75. className="btn m-1"
  76. disabled={isCurrConvGenerating}
  77. >
  78. <svg
  79. xmlns="http://www.w3.org/2000/svg"
  80. width="16"
  81. height="16"
  82. fill="currentColor"
  83. className="bi bi-three-dots-vertical"
  84. viewBox="0 0 16 16"
  85. >
  86. <path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0" />
  87. </svg>
  88. </button>
  89. {/* dropdown menu */}
  90. <ul
  91. tabIndex={0}
  92. className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
  93. >
  94. <li onClick={downloadConversation}>
  95. <a>Download</a>
  96. </li>
  97. <li className="text-error" onClick={removeConversation}>
  98. <a>Delete</a>
  99. </li>
  100. </ul>
  101. </div>
  102. <div className="tooltip tooltip-bottom" data-tip="Settings">
  103. <button className="btn" onClick={() => setShowSettingDialog(true)}>
  104. {/* settings button */}
  105. <svg
  106. xmlns="http://www.w3.org/2000/svg"
  107. width="16"
  108. height="16"
  109. fill="currentColor"
  110. className="bi bi-gear"
  111. viewBox="0 0 16 16"
  112. >
  113. <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0" />
  114. <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z" />
  115. </svg>
  116. </button>
  117. </div>
  118. {/* theme controller is copied from https://daisyui.com/components/theme-controller/ */}
  119. <div className="tooltip tooltip-bottom" data-tip="Themes">
  120. <div className="dropdown dropdown-end dropdown-bottom">
  121. <div tabIndex={0} role="button" className="btn m-1">
  122. <svg
  123. xmlns="http://www.w3.org/2000/svg"
  124. width="16"
  125. height="16"
  126. fill="currentColor"
  127. className="bi bi-palette2"
  128. viewBox="0 0 16 16"
  129. >
  130. <path d="M0 .5A.5.5 0 0 1 .5 0h5a.5.5 0 0 1 .5.5v5.277l4.147-4.131a.5.5 0 0 1 .707 0l3.535 3.536a.5.5 0 0 1 0 .708L10.261 10H15.5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5H3a3 3 0 0 1-2.121-.879A3 3 0 0 1 0 13.044m6-.21 7.328-7.3-2.829-2.828L6 7.188zM4.5 13a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0M15 15v-4H9.258l-4.015 4zM0 .5v12.495zm0 12.495V13z" />
  131. </svg>
  132. </div>
  133. <ul
  134. tabIndex={0}
  135. className="dropdown-content bg-base-300 rounded-box z-[1] w-52 p-2 shadow-2xl h-80 overflow-y-auto"
  136. >
  137. <li>
  138. <button
  139. className={classNames({
  140. 'btn btn-sm btn-block btn-ghost justify-start': true,
  141. 'btn-active': selectedTheme === 'auto',
  142. })}
  143. onClick={() => setTheme('auto')}
  144. >
  145. auto
  146. </button>
  147. </li>
  148. {THEMES.map((theme) => (
  149. <li key={theme}>
  150. <input
  151. type="radio"
  152. name="theme-dropdown"
  153. className="theme-controller btn btn-sm btn-block btn-ghost justify-start"
  154. aria-label={theme}
  155. value={theme}
  156. checked={selectedTheme === theme}
  157. onChange={(e) => e.target.checked && setTheme(theme)}
  158. />
  159. </li>
  160. ))}
  161. </ul>
  162. </div>
  163. </div>
  164. </div>
  165. <SettingDialog
  166. show={showSettingDialog}
  167. onClose={() => setShowSettingDialog(false)}
  168. />
  169. </div>
  170. );
  171. }