mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-06-20 08:48:35 +00:00
Compare commits
22 Commits
v1.10.0-rc
...
21400dcafc
Author | SHA1 | Date | |
---|---|---|---|
21400dcafc | |||
89b5229ce9 | |||
7756340dd9 | |||
bbd2e9c359 | |||
a32eb1dda3 | |||
aa834f7f04 | |||
064c0fbe42 | |||
bf4cf8eaeb | |||
a24992a3db | |||
d584067bb1 | |||
df4350f966 | |||
652ca2fdf4 | |||
216576128d | |||
bb3f180583 | |||
4d24d73161 | |||
2e166c217b | |||
4c73caadf6 | |||
5f0b87f4a9 | |||
8aaee2c40c | |||
115e6b2a71 | |||
a5c79c92ed | |||
db3cea446e |
20
README.md
20
README.md
@ -1,7 +1,22 @@
|
|||||||
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
||||||
|
|
||||||
[](https://discord.gg/26aArMy8tT)
|
<div align="center" markdown="1">
|
||||||
|
<sup>Special thanks to:</sup>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="https://www.warp.dev/perplexica">
|
||||||
|
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica)
|
||||||
|
|
||||||
|
[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
[](https://discord.gg/26aArMy8tT)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -44,7 +59,7 @@ Want to know more about its architecture and how it works? You can read it [here
|
|||||||
- **Normal Mode:** Processes your query and performs a web search.
|
- **Normal Mode:** Processes your query and performs a web search.
|
||||||
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
|
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
|
||||||
- **All Mode:** Searches the entire web to find the best results.
|
- **All Mode:** Searches the entire web to find the best results.
|
||||||
- **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web.
|
- **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web.
|
||||||
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
||||||
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
|
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
|
||||||
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
|
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
|
||||||
@ -143,6 +158,7 @@ You can access Perplexica over your home network by following our networking gui
|
|||||||
|
|
||||||
## One-Click Deployment
|
## One-Click Deployment
|
||||||
|
|
||||||
|
[](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
|
||||||
[](https://repocloud.io/details/?app_id=267)
|
[](https://repocloud.io/details/?app_id=267)
|
||||||
|
|
||||||
## Upcoming Features
|
## Upcoming Features
|
||||||
|
@ -10,23 +10,25 @@ To update Perplexica to the latest version, follow these steps:
|
|||||||
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Navigate to the Project Directory.
|
2. Navigate to the project directory.
|
||||||
|
|
||||||
3. Pull latest images from registry.
|
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
|
||||||
|
|
||||||
|
4. Pull the latest images from the registry.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull
|
docker compose pull
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Update and Recreate containers.
|
5. Update and recreate the containers.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Once the command completes running go to http://localhost:3000 and verify the latest changes.
|
6. Once the command completes, go to http://localhost:3000 and verify the latest changes.
|
||||||
|
|
||||||
## For non Docker users
|
## For non-Docker users
|
||||||
|
|
||||||
1. Clone the latest version of Perplexica from GitHub:
|
1. Clone the latest version of Perplexica from GitHub:
|
||||||
|
|
||||||
@ -34,7 +36,14 @@ To update Perplexica to the latest version, follow these steps:
|
|||||||
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Navigate to the Project Directory
|
2. Navigate to the project directory.
|
||||||
3. Execute `npm i` in both the `ui` folder and the root directory.
|
|
||||||
4. Once packages are updated, execute `npm run build` in both the `ui` folder and the root directory.
|
3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
|
||||||
5. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
|
|
||||||
|
4. Execute `npm i` in both the `ui` folder and the root directory.
|
||||||
|
|
||||||
|
5. Once the packages are updated, execute `npm run build` in both the `ui` folder and the root directory.
|
||||||
|
|
||||||
|
6. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
@ -85,10 +85,12 @@ router.post('/', async (req, res) => {
|
|||||||
if (body.chatModel?.provider === 'custom_openai') {
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
llm = new ChatOpenAI({
|
llm = new ChatOpenAI({
|
||||||
modelName: body.chatModel?.model || getCustomOpenaiModelName(),
|
modelName: body.chatModel?.model || getCustomOpenaiModelName(),
|
||||||
openAIApiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
openAIApiKey:
|
||||||
|
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(),
|
baseURL:
|
||||||
|
body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(),
|
||||||
},
|
},
|
||||||
}) as unknown as BaseChatModel;
|
}) as unknown as BaseChatModel;
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -65,7 +65,7 @@ export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
|
|||||||
const splittedText = await splitter.splitText(parsedText);
|
const splittedText = await splitter.splitText(parsedText);
|
||||||
const title = res.data
|
const title = res.data
|
||||||
.toString('utf8')
|
.toString('utf8')
|
||||||
.match(/<title>(.*?)<\/title>/)?.[1];
|
.match(/<title.*>(.*?)<\/title>/)?.[1];
|
||||||
|
|
||||||
const linkDocs = splittedText.map((text) => {
|
const linkDocs = splittedText.map((text) => {
|
||||||
return new Document({
|
return new Document({
|
||||||
|
@ -223,11 +223,11 @@ const Page = () => {
|
|||||||
setChatModels(data.chatModelProviders || {});
|
setChatModels(data.chatModelProviders || {});
|
||||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||||
|
|
||||||
const currentProvider = selectedChatModelProvider;
|
const currentChatProvider = selectedChatModelProvider;
|
||||||
const newProviders = Object.keys(data.chatModelProviders || {});
|
const newChatProviders = Object.keys(data.chatModelProviders || {});
|
||||||
|
|
||||||
if (!currentProvider && newProviders.length > 0) {
|
if (!currentChatProvider && newChatProviders.length > 0) {
|
||||||
const firstProvider = newProviders[0];
|
const firstProvider = newChatProviders[0];
|
||||||
const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name;
|
const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name;
|
||||||
|
|
||||||
if (firstModel) {
|
if (firstModel) {
|
||||||
@ -237,11 +237,11 @@ const Page = () => {
|
|||||||
localStorage.setItem('chatModel', firstModel);
|
localStorage.setItem('chatModel', firstModel);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
currentProvider &&
|
currentChatProvider &&
|
||||||
(!data.chatModelProviders ||
|
(!data.chatModelProviders ||
|
||||||
!data.chatModelProviders[currentProvider] ||
|
!data.chatModelProviders[currentChatProvider] ||
|
||||||
!Array.isArray(data.chatModelProviders[currentProvider]) ||
|
!Array.isArray(data.chatModelProviders[currentChatProvider]) ||
|
||||||
data.chatModelProviders[currentProvider].length === 0)
|
data.chatModelProviders[currentChatProvider].length === 0)
|
||||||
) {
|
) {
|
||||||
const firstValidProvider = Object.entries(
|
const firstValidProvider = Object.entries(
|
||||||
data.chatModelProviders || {},
|
data.chatModelProviders || {},
|
||||||
@ -267,6 +267,55 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentEmbeddingProvider = selectedEmbeddingModelProvider;
|
||||||
|
const newEmbeddingProviders = Object.keys(
|
||||||
|
data.embeddingModelProviders || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentEmbeddingProvider && newEmbeddingProviders.length > 0) {
|
||||||
|
const firstProvider = newEmbeddingProviders[0];
|
||||||
|
const firstModel =
|
||||||
|
data.embeddingModelProviders[firstProvider]?.[0]?.name;
|
||||||
|
|
||||||
|
if (firstModel) {
|
||||||
|
setSelectedEmbeddingModelProvider(firstProvider);
|
||||||
|
setSelectedEmbeddingModel(firstModel);
|
||||||
|
localStorage.setItem('embeddingModelProvider', firstProvider);
|
||||||
|
localStorage.setItem('embeddingModel', firstModel);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
currentEmbeddingProvider &&
|
||||||
|
(!data.embeddingModelProviders ||
|
||||||
|
!data.embeddingModelProviders[currentEmbeddingProvider] ||
|
||||||
|
!Array.isArray(
|
||||||
|
data.embeddingModelProviders[currentEmbeddingProvider],
|
||||||
|
) ||
|
||||||
|
data.embeddingModelProviders[currentEmbeddingProvider].length === 0)
|
||||||
|
) {
|
||||||
|
const firstValidProvider = Object.entries(
|
||||||
|
data.embeddingModelProviders || {},
|
||||||
|
).find(
|
||||||
|
([_, models]) => Array.isArray(models) && models.length > 0,
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (firstValidProvider) {
|
||||||
|
setSelectedEmbeddingModelProvider(firstValidProvider);
|
||||||
|
setSelectedEmbeddingModel(
|
||||||
|
data.embeddingModelProviders[firstValidProvider][0].name,
|
||||||
|
);
|
||||||
|
localStorage.setItem('embeddingModelProvider', firstValidProvider);
|
||||||
|
localStorage.setItem(
|
||||||
|
'embeddingModel',
|
||||||
|
data.embeddingModelProviders[firstValidProvider][0].name,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedEmbeddingModelProvider(null);
|
||||||
|
setSelectedEmbeddingModel(null);
|
||||||
|
localStorage.removeItem('embeddingModelProvider');
|
||||||
|
localStorage.removeItem('embeddingModel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setConfig(data);
|
setConfig(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +327,10 @@ const Page = () => {
|
|||||||
localStorage.setItem('chatModelProvider', value);
|
localStorage.setItem('chatModelProvider', value);
|
||||||
} else if (key === 'chatModel') {
|
} else if (key === 'chatModel') {
|
||||||
localStorage.setItem('chatModel', value);
|
localStorage.setItem('chatModel', value);
|
||||||
|
} else if (key === 'embeddingModelProvider') {
|
||||||
|
localStorage.setItem('embeddingModelProvider', value);
|
||||||
|
} else if (key === 'embeddingModel') {
|
||||||
|
localStorage.setItem('embeddingModel', value);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save:', err);
|
console.error('Failed to save:', err);
|
||||||
@ -436,7 +489,6 @@ const Page = () => {
|
|||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setSelectedChatModelProvider(value);
|
setSelectedChatModelProvider(value);
|
||||||
saveConfig('chatModelProvider', value);
|
saveConfig('chatModelProvider', value);
|
||||||
// Auto-select first model of new provider
|
|
||||||
const firstModel =
|
const firstModel =
|
||||||
config.chatModelProviders[value]?.[0]?.name;
|
config.chatModelProviders[value]?.[0]?.name;
|
||||||
if (firstModel) {
|
if (firstModel) {
|
||||||
@ -511,12 +563,16 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Model name"
|
placeholder="Model name"
|
||||||
defaultValue={config.customOpenaiModelName}
|
value={config.customOpenaiModelName}
|
||||||
onChange={(e) =>
|
isSaving={savingStates['customOpenaiModelName']}
|
||||||
setConfig({
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
...config,
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
customOpenaiModelName: e.target.value,
|
customOpenaiModelName: e.target.value,
|
||||||
})
|
}));
|
||||||
|
}}
|
||||||
|
onSave={(value) =>
|
||||||
|
saveConfig('customOpenaiModelName', value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -527,12 +583,16 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI API Key"
|
placeholder="Custom OpenAI API Key"
|
||||||
defaultValue={config.customOpenaiApiKey}
|
value={config.customOpenaiApiKey}
|
||||||
onChange={(e) =>
|
isSaving={savingStates['customOpenaiApiKey']}
|
||||||
setConfig({
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
...config,
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
customOpenaiApiKey: e.target.value,
|
customOpenaiApiKey: e.target.value,
|
||||||
})
|
}));
|
||||||
|
}}
|
||||||
|
onSave={(value) =>
|
||||||
|
saveConfig('customOpenaiApiKey', value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -543,17 +603,96 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI Base URL"
|
placeholder="Custom OpenAI Base URL"
|
||||||
defaultValue={config.customOpenaiApiUrl}
|
value={config.customOpenaiApiUrl}
|
||||||
onChange={(e) =>
|
isSaving={savingStates['customOpenaiApiUrl']}
|
||||||
setConfig({
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
...config,
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
customOpenaiApiUrl: e.target.value,
|
customOpenaiApiUrl: e.target.value,
|
||||||
})
|
}));
|
||||||
|
}}
|
||||||
|
onSave={(value) =>
|
||||||
|
saveConfig('customOpenaiApiUrl', value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{config.embeddingModelProviders && (
|
||||||
|
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Embedding Model Provider
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={selectedEmbeddingModelProvider ?? undefined}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSelectedEmbeddingModelProvider(value);
|
||||||
|
saveConfig('embeddingModelProvider', value);
|
||||||
|
const firstModel =
|
||||||
|
config.embeddingModelProviders[value]?.[0]?.name;
|
||||||
|
if (firstModel) {
|
||||||
|
setSelectedEmbeddingModel(firstModel);
|
||||||
|
saveConfig('embeddingModel', firstModel);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={Object.keys(config.embeddingModelProviders).map(
|
||||||
|
(provider) => ({
|
||||||
|
value: provider,
|
||||||
|
label:
|
||||||
|
provider.charAt(0).toUpperCase() +
|
||||||
|
provider.slice(1),
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedEmbeddingModelProvider && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Embedding Model
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={selectedEmbeddingModel ?? undefined}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSelectedEmbeddingModel(value);
|
||||||
|
saveConfig('embeddingModel', value);
|
||||||
|
}}
|
||||||
|
options={(() => {
|
||||||
|
const embeddingModelProvider =
|
||||||
|
config.embeddingModelProviders[
|
||||||
|
selectedEmbeddingModelProvider
|
||||||
|
];
|
||||||
|
return embeddingModelProvider
|
||||||
|
? embeddingModelProvider.length > 0
|
||||||
|
? embeddingModelProvider.map((model) => ({
|
||||||
|
value: model.name,
|
||||||
|
label: model.displayName,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: 'No models available',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label:
|
||||||
|
'Invalid provider, please check backend logs',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="API Keys">
|
<SettingsSection title="API Keys">
|
||||||
|
@ -68,7 +68,7 @@ const MessageBox = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8', 'break-words')}>
|
||||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||||
{message.content}
|
{message.content}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -110,7 +110,7 @@ const Attach = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => fileInputRef.current.click()}
|
||||||
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -128,7 +128,7 @@ const Attach = ({
|
|||||||
setFiles([]);
|
setFiles([]);
|
||||||
setFileIds([]);
|
setFileIds([]);
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">Clear</p>
|
||||||
@ -145,7 +145,7 @@ const Attach = ({
|
|||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
||||||
'...' +
|
'...' +
|
||||||
|
@ -82,7 +82,7 @@ const AttachSmall = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => fileInputRef.current.click()}
|
||||||
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -100,7 +100,7 @@ const AttachSmall = ({
|
|||||||
setFiles([]);
|
setFiles([]);
|
||||||
setFileIds([]);
|
setFileIds([]);
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">Clear</p>
|
||||||
@ -117,7 +117,7 @@ const AttachSmall = ({
|
|||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
||||||
'...' +
|
'...' +
|
||||||
|
Reference in New Issue
Block a user