Compare commits

..

24 Commits
0.3.0 ... 0.3.1

Author SHA1 Message Date
John Wang
a1c6cecf10 feat: bump to 0.3.1 (#253) 2023-05-30 11:31:22 +08:00
Joel
c5ccf382df chore: input area highlight and moblie hide tooltip (#251) 2023-05-30 11:16:31 +08:00
crazywoola
8358d0abfa fix: config file lint error (#250) 2023-05-30 10:32:26 +08:00
Joel
bad3b14438 fix: member invite text (#249) 2023-05-30 09:59:05 +08:00
KVOJJJin
f42ef494f8 Fix: correct links in app list (#248) 2023-05-30 08:08:33 +08:00
John Wang
bb7f454ecd fix: dataset desc npe (#246) 2023-05-29 19:56:36 +08:00
John Wang
7f48fadd41 fix: prompt template parantheses select error (#244) 2023-05-29 19:10:31 +08:00
John Wang
af2138e8b8 fix: json parse in router chain output (#243) 2023-05-29 18:25:01 +08:00
Joel
091beffae7 feat: add code style (#242) 2023-05-29 17:49:01 +08:00
Joel
408fb502a1 fix: no var text still show split line (#239) 2023-05-29 14:35:21 +08:00
Joel
7660539689 fix: markdown code always show scrollbar (#237) 2023-05-29 14:05:59 +08:00
Joel
5a6061ff61 chore: handle sentry warning (#236) 2023-05-29 13:58:32 +08:00
Joel
970950e3a8 feat: support select multi datasets (#235) 2023-05-29 13:52:56 +08:00
Joel
431b2fd4a8 Feat: add sentry (#234) 2023-05-29 11:38:24 +08:00
John Wang
88545184be feat: support multi datasets router chain mode (#231) 2023-05-28 22:44:54 +08:00
John Wang
2c23caacd4 fix: introduction key error (#221) 2023-05-26 20:49:38 +08:00
Joel
9edea9bc49 fix: one chinese character cost token nums (#219) 2023-05-26 16:24:59 +08:00
Yuhao
d43279a1cc fix: robot emoji (#217) 2023-05-26 15:26:56 +08:00
Joel
10848d74a0 fix: changelog link (#216) 2023-05-26 10:22:35 +08:00
crazywoola
f9df23a091 fix: default icon (#213) 2023-05-26 09:55:37 +08:00
Joel
17a1c05728 fix: var highlight problme (#214) 2023-05-25 23:38:06 +08:00
Joel
66782ef19c chore: title support i18n (#212) 2023-05-25 22:13:43 +08:00
Joel
fb7f509e5c chore: show explore entrance (#211) 2023-05-25 21:49:12 +08:00
John Wang
1a5acf43aa Fix/shared lock (#210) 2023-05-25 21:31:11 +08:00
48 changed files with 593 additions and 217 deletions

1
.gitignore vendored
View File

@@ -130,7 +130,6 @@ dmypy.json
.idea/'
.DS_Store
.vscode
# Intellij IDEA Files
.idea/

View File

@@ -78,7 +78,7 @@ class Config:
self.CONSOLE_URL = get_env('CONSOLE_URL')
self.API_URL = get_env('API_URL')
self.APP_URL = get_env('APP_URL')
self.CURRENT_VERSION = "0.3.0"
self.CURRENT_VERSION = "0.3.1"
self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV')

View File

@@ -34,5 +34,9 @@ class DatasetIndexToolCallbackHandler(IndexToolCallbackHandler):
db.session.query(DocumentSegment).filter(
DocumentSegment.dataset_id == self.dataset_id,
DocumentSegment.index_node_id == index_node_id
).update({DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False)
).update(
{DocumentSegment.hit_count: DocumentSegment.hit_count + 1},
synchronize_session=False
)
db.session.commit()

View File

@@ -0,0 +1,135 @@
"""Base classes for LLM-powered router chains."""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Type, cast, NamedTuple
from langchain.chains.base import Chain
from pydantic import root_validator
from langchain.chains import LLMChain
from langchain.prompts import BasePromptTemplate
from langchain.schema import BaseOutputParser, OutputParserException, BaseLanguageModel
class Route(NamedTuple):
destination: Optional[str]
next_inputs: Dict[str, Any]
class LLMRouterChain(Chain):
"""A router chain that uses an LLM chain to perform routing."""
llm_chain: LLMChain
"""LLM chain used to perform routing"""
@root_validator()
def validate_prompt(cls, values: dict) -> dict:
prompt = values["llm_chain"].prompt
if prompt.output_parser is None:
raise ValueError(
"LLMRouterChain requires base llm_chain prompt to have an output"
" parser that converts LLM text output to a dictionary with keys"
" 'destination' and 'next_inputs'. Received a prompt with no output"
" parser."
)
return values
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the LLM chain prompt expects.
:meta private:
"""
return self.llm_chain.input_keys
def _validate_outputs(self, outputs: Dict[str, Any]) -> None:
super()._validate_outputs(outputs)
if not isinstance(outputs["next_inputs"], dict):
raise ValueError
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
output = cast(
Dict[str, Any],
self.llm_chain.predict_and_parse(**inputs),
)
return output
@classmethod
def from_llm(
cls, llm: BaseLanguageModel, prompt: BasePromptTemplate, **kwargs: Any
) -> LLMRouterChain:
"""Convenience constructor."""
llm_chain = LLMChain(llm=llm, prompt=prompt)
return cls(llm_chain=llm_chain, **kwargs)
@property
def output_keys(self) -> List[str]:
return ["destination", "next_inputs"]
def route(self, inputs: Dict[str, Any]) -> Route:
result = self(inputs)
return Route(result["destination"], result["next_inputs"])
class RouterOutputParser(BaseOutputParser[Dict[str, str]]):
"""Parser for output of router chain int he multi-prompt chain."""
default_destination: str = "DEFAULT"
next_inputs_type: Type = str
next_inputs_inner_key: str = "input"
def parse_json_markdown(self, json_string: str) -> dict:
# Remove the triple backticks if present
start_index = json_string.find("```json")
end_index = json_string.find("```", start_index + len("```json"))
if start_index != -1 and end_index != -1:
extracted_content = json_string[start_index + len("```json"):end_index].strip()
# Parse the JSON string into a Python dictionary
parsed = json.loads(extracted_content)
else:
raise Exception("Could not find JSON block in the output.")
return parsed
def parse_and_check_json_markdown(self, text: str, expected_keys: List[str]) -> dict:
try:
json_obj = self.parse_json_markdown(text)
except json.JSONDecodeError as e:
raise OutputParserException(f"Got invalid JSON object. Error: {e}")
for key in expected_keys:
if key not in json_obj:
raise OutputParserException(
f"Got invalid return object. Expected key `{key}` "
f"to be present, but got {json_obj}"
)
return json_obj
def parse(self, text: str) -> Dict[str, Any]:
try:
expected_keys = ["destination", "next_inputs"]
parsed = self.parse_and_check_json_markdown(text, expected_keys)
if not isinstance(parsed["destination"], str):
raise ValueError("Expected 'destination' to be a string.")
if not isinstance(parsed["next_inputs"], self.next_inputs_type):
raise ValueError(
f"Expected 'next_inputs' to be {self.next_inputs_type}."
)
parsed["next_inputs"] = {self.next_inputs_inner_key: parsed["next_inputs"]}
if (
parsed["destination"].strip().lower()
== self.default_destination.lower()
):
parsed["destination"] = None
else:
parsed["destination"] = parsed["destination"].strip()
return parsed
except Exception as e:
raise OutputParserException(
f"Parsing text\n{text}\n raised following error:\n{e}"
)

View File

@@ -1,18 +1,18 @@
from typing import Optional, List
from langchain.callbacks import SharedCallbackManager
from langchain.callbacks import SharedCallbackManager, CallbackManager
from langchain.chains import SequentialChain
from langchain.chains.base import Chain
from langchain.memory.chat_memory import BaseChatMemory
from core.agent.agent_builder import AgentBuilder
from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.main_chain_gather_callback_handler import MainChainGatherCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.chain_builder import ChainBuilder
from core.constant import llm_constant
from core.chain.multi_dataset_router_chain import MultiDatasetRouterChain
from core.conversation_message_task import ConversationMessageTask
from core.tool.dataset_tool_builder import DatasetToolBuilder
from extensions.ext_database import db
from models.dataset import Dataset
class MainChainBuilder:
@@ -31,8 +31,7 @@ class MainChainBuilder:
tenant_id=tenant_id,
agent_mode=agent_mode,
memory=memory,
dataset_tool_callback_handler=DatasetToolCallbackHandler(conversation_message_task),
agent_loop_gather_callback_handler=chain_callback_handler.agent_loop_gather_callback_handler
conversation_message_task=conversation_message_task
)
chains += tool_chains
@@ -59,15 +58,15 @@ class MainChainBuilder:
@classmethod
def get_agent_chains(cls, tenant_id: str, agent_mode: dict, memory: Optional[BaseChatMemory],
dataset_tool_callback_handler: DatasetToolCallbackHandler,
agent_loop_gather_callback_handler: AgentLoopGatherCallbackHandler):
conversation_message_task: ConversationMessageTask):
# agent mode
chains = []
if agent_mode and agent_mode.get('enabled'):
tools = agent_mode.get('tools', [])
pre_fixed_chains = []
agent_tools = []
# agent_tools = []
datasets = []
for tool in tools:
tool_type = list(tool.keys())[0]
tool_config = list(tool.values())[0]
@@ -76,34 +75,27 @@ class MainChainBuilder:
if chain:
pre_fixed_chains.append(chain)
elif tool_type == "dataset":
dataset_tool = DatasetToolBuilder.build_dataset_tool(
tenant_id=tenant_id,
dataset_id=tool_config.get("id"),
response_mode='no_synthesizer', # "compact"
callback_handler=dataset_tool_callback_handler
)
# get dataset from dataset id
dataset = db.session.query(Dataset).filter(
Dataset.tenant_id == tenant_id,
Dataset.id == tool_config.get("id")
).first()
if dataset_tool:
agent_tools.append(dataset_tool)
if dataset:
datasets.append(dataset)
# add pre-fixed chains
chains += pre_fixed_chains
if len(agent_tools) == 1:
if len(datasets) > 0:
# tool to chain
tool_chain = ChainBuilder.to_tool_chain(tool=agent_tools[0], output_key='tool_output')
chains.append(tool_chain)
elif len(agent_tools) > 1:
# build agent config
agent_chain = AgentBuilder.to_agent_chain(
multi_dataset_router_chain = MultiDatasetRouterChain.from_datasets(
tenant_id=tenant_id,
tools=agent_tools,
memory=memory,
dataset_tool_callback_handler=dataset_tool_callback_handler,
agent_loop_gather_callback_handler=agent_loop_gather_callback_handler
datasets=datasets,
conversation_message_task=conversation_message_task,
callback_manager=CallbackManager([DifyStdOutCallbackHandler()])
)
chains.append(agent_chain)
chains.append(multi_dataset_router_chain)
final_output_key = cls.get_chains_output_key(chains)

View File

@@ -0,0 +1,140 @@
from typing import Mapping, List, Dict, Any, Optional
from langchain import LLMChain, PromptTemplate, ConversationChain
from langchain.callbacks import CallbackManager
from langchain.chains.base import Chain
from langchain.schema import BaseLanguageModel
from pydantic import Extra
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.llm_router_chain import LLMRouterChain, RouterOutputParser
from core.conversation_message_task import ConversationMessageTask
from core.llm.llm_builder import LLMBuilder
from core.tool.dataset_tool_builder import DatasetToolBuilder
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from models.dataset import Dataset
MULTI_PROMPT_ROUTER_TEMPLATE = """
Given a raw text input to a language model select the model prompt best suited for \
the input. You will be given the names of the available prompts and a description of \
what the prompt is best suited for. You may also revise the original input if you \
think that revising it will ultimately lead to a better response from the language \
model.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
```json
{{{{
"destination": string \\ name of the prompt to use or "DEFAULT"
"next_inputs": string \\ a potentially modified version of the original input
}}}}
```
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR \
it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any \
modifications are needed.
<< CANDIDATE PROMPTS >>
{destinations}
<< INPUT >>
{{input}}
<< OUTPUT >>
"""
class MultiDatasetRouterChain(Chain):
"""Use a single chain to route an input to one of multiple candidate chains."""
router_chain: LLMRouterChain
"""Chain for deciding a destination chain and the input to it."""
dataset_tools: Mapping[str, EnhanceLlamaIndexTool]
"""Map of name to candidate chains that inputs can be routed to."""
class Config:
"""Configuration for this pydantic object."""
extra = Extra.forbid
arbitrary_types_allowed = True
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the router chain prompt expects.
:meta private:
"""
return self.router_chain.input_keys
@property
def output_keys(self) -> List[str]:
return ["text"]
@classmethod
def from_datasets(
cls,
tenant_id: str,
datasets: List[Dataset],
conversation_message_task: ConversationMessageTask,
**kwargs: Any,
):
"""Convenience constructor for instantiating from destination prompts."""
llm_callback_manager = CallbackManager([DifyStdOutCallbackHandler()])
llm = LLMBuilder.to_llm(
tenant_id=tenant_id,
model_name='gpt-3.5-turbo',
temperature=0,
max_tokens=1024,
callback_manager=llm_callback_manager
)
destinations = ["{}: {}".format(d.id, d.description.replace('\n', ' ') if d.description
else ('useful for when you want to answer queries about the ' + d.name))
for d in datasets]
destinations_str = "\n".join(destinations)
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
destinations=destinations_str
)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
dataset_tools = {}
for dataset in datasets:
dataset_tool = DatasetToolBuilder.build_dataset_tool(
dataset=dataset,
response_mode='no_synthesizer', # "compact"
callback_handler=DatasetToolCallbackHandler(conversation_message_task)
)
dataset_tools[dataset.id] = dataset_tool
return cls(
router_chain=router_chain,
dataset_tools=dataset_tools,
**kwargs,
)
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
if len(self.dataset_tools) == 0:
return {"text": ''}
elif len(self.dataset_tools) == 1:
return {"text": next(iter(self.dataset_tools.values())).run(inputs['input'])}
route = self.router_chain.route(inputs)
if not route.destination:
return {"text": ''}
elif route.destination in self.dataset_tools:
return {"text": self.dataset_tools[route.destination].run(
route.next_inputs['input']
)}
else:
raise ValueError(
f"Received invalid destination chain name '{route.destination}'"
)

View File

@@ -1,14 +1,17 @@
import logging
from typing import Optional, List, Union, Tuple
from langchain.callbacks import CallbackManager
from langchain.chat_models.base import BaseChatModel
from langchain.llms import BaseLLM
from langchain.schema import BaseMessage, BaseLanguageModel, HumanMessage
from requests.exceptions import ChunkedEncodingError
from core.constant import llm_constant
from core.callback_handler.llm_callback_handler import LLMCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStreamingStdOutCallbackHandler, \
DifyStdOutCallbackHandler
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException, PubHandler
from core.llm.error import LLMBadRequestError
from core.llm.llm_builder import LLMBuilder
from core.chain.main_chain_builder import MainChainBuilder
@@ -84,6 +87,11 @@ class Completion:
)
except ConversationTaskStoppedException:
return
except ChunkedEncodingError as e:
# Interrupt by LLM (like OpenAI), handle it.
logging.warning(f'ChunkedEncodingError: {e}')
conversation_message_task.end()
return
@classmethod
def run_final_llm(cls, tenant_id: str, mode: str, app_model_config: AppModelConfig, query: str, inputs: dict,

View File

@@ -80,7 +80,10 @@ class ConversationMessageTask:
if introduction:
prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction))
prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs}
introduction = prompt_template.format(**prompt_inputs)
try:
introduction = prompt_template.format(**prompt_inputs)
except KeyError:
pass
if self.app_model_config.pre_prompt:
pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt)
@@ -171,7 +174,7 @@ class ConversationMessageTask:
)
if not by_stopped:
self._pub_handler.pub_end()
self.end()
def update_provider_quota(self):
llm_provider_service = LLMProviderService(
@@ -268,6 +271,9 @@ class ConversationMessageTask:
total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price
return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP)
def end(self):
self._pub_handler.pub_end()
class PubHandler:
def __init__(self, user: Union[Account | EndUser], task_id: str,

View File

@@ -32,6 +32,6 @@ class PromptBuilder:
@classmethod
def process_template(cls, template: str):
processed_template = re.sub(r'\{(.+?)\}', r'\1', template)
processed_template = re.sub(r'\{\{(.+?)\}\}', r'{\1}', processed_template)
processed_template = re.sub(r'\{([a-zA-Z_]\w+?)\}', r'\1', template)
processed_template = re.sub(r'\{\{([a-zA-Z_]\w+?)\}\}', r'{\1}', processed_template)
return processed_template

View File

@@ -10,24 +10,14 @@ from core.index.keyword_table_index import KeywordTableIndex
from core.index.vector_index import VectorIndex
from core.prompt.prompts import QUERY_KEYWORD_EXTRACT_TEMPLATE
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from extensions.ext_database import db
from models.dataset import Dataset
class DatasetToolBuilder:
@classmethod
def build_dataset_tool(cls, tenant_id: str, dataset_id: str,
def build_dataset_tool(cls, dataset: Dataset,
response_mode: str = "no_synthesizer",
callback_handler: Optional[DatasetToolCallbackHandler] = None):
# get dataset from dataset id
dataset = db.session.query(Dataset).filter(
Dataset.tenant_id == tenant_id,
Dataset.id == dataset_id
).first()
if not dataset:
return None
if dataset.indexing_technique == "economy":
# use keyword table query
index = KeywordTableIndex(dataset=dataset).query_index
@@ -65,7 +55,7 @@ class DatasetToolBuilder:
index_tool_config = IndexToolConfig(
index=index,
name=f"dataset-{dataset_id}",
name=f"dataset-{dataset.id}",
description=description,
index_query_kwargs=query_kwargs,
tool_kwargs={
@@ -75,7 +65,7 @@ class DatasetToolBuilder:
# return_direct: Whether to return LLM results directly or process the output data with an Output Parser
)
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset_id)
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset.id)
return EnhanceLlamaIndexTool.from_tool_config(
tool_config=index_tool_config,

View File

@@ -2,7 +2,7 @@ version: '3.1'
services:
# API service
api:
image: langgenius/dify-api:0.3.0
image: langgenius/dify-api:0.3.1
restart: always
environment:
# Startup mode, 'api' starts the API server.
@@ -110,7 +110,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:0.3.0
image: langgenius/dify-api:0.3.1
restart: always
environment:
# Startup mode, 'worker' starts the Celery worker for processing the queue.
@@ -156,7 +156,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:0.3.0
image: langgenius/dify-web:0.3.1
restart: always
environment:
EDITION: SELF_HOSTED

7
web/.eslintignore Normal file
View File

@@ -0,0 +1,7 @@
/**/node_modules/*
node_modules/
dist/
build/
out/
.next/

View File

@@ -23,6 +23,6 @@
]
}
],
"react-hooks/exhaustive-deps": "warning"
"react-hooks/exhaustive-deps": "warn"
}
}
}

4
web/.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd ./web && npx lint-staged

25
web/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.format.enable": true,
"[python]": {
"editor.formatOnType": true
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View File

@@ -7,9 +7,9 @@ export type IAppDetail = {
const AppDetail: FC<IAppDetail> = ({ children }) => {
return (
<>
{children}
</>
<>
{children}
</>
)
}

View File

@@ -9,6 +9,7 @@ import { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps'
import { useSelector } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useTranslation } from 'react-i18next'
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more)
@@ -17,12 +18,14 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
}
const Apps = () => {
const { t } = useTranslation()
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
const loadingStateRef = useRef(false)
const pageContainerRef = useSelector(state => state.pageContainerRef)
const anchorRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
document.title = `${t('app.title')} - Dify`;
if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()

View File

@@ -37,7 +37,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
// Emoji Picker
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
@@ -102,7 +102,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}

View File

@@ -14,23 +14,19 @@ const AppList = async () => {
<footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
{/*<p className='mt-3 text-sm'>*/}
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/}
{/* {t('roadmap')}*/}
{/* <span className={style.linkIcon} />*/}
{/* </a>*/}
{/*</p>*/}
{/* <p className='mt-3 text-sm'> */}
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}> */}
{/* {t('roadmap')} */}
{/* <span className={style.linkIcon} /> */}
{/* </a> */}
{/* </p> */}
<div className='flex items-center gap-2 mt-3'>
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
</div>
</footer>
</div >
)
}
export const metadata = {
title: 'Apps - Dify',
}
export default AppList

View File

@@ -8,6 +8,8 @@ import { UserCircleIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import { randomString } from '../../app-sidebar/basic'
import s from './style.module.css'
import LoadingAnim from './loading-anim'
import CopyBtn from './copy-btn'
import Tooltip from '@/app/components/base/tooltip'
import { ToastContext } from '@/app/components/base/toast'
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
@@ -15,9 +17,8 @@ import Button from '@/app/components/base/button'
import type { Annotation, MessageRating } from '@/models/log'
import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from './loading-anim'
import { formatNumber } from '@/utils/format'
import CopyBtn from './copy-btn'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
const stopIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -285,8 +286,8 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
<div key={id}>
<div className='flex items-start'>
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponsing &&
<div className={s.typeingIcon}>
{isResponsing
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
@@ -301,13 +302,15 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
</div>
)}
{(isResponsing && !content) ? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
) : (
<Markdown content={content} />
)}
{(isResponsing && !content)
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
)
: (
<Markdown content={content} />
)}
{!showEdit
? (annotation?.content
&& <>
@@ -384,13 +387,15 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar
</div>
{more && <MoreInfo more={more} isQuestion={true} />}
</div>
{useCurrentUserAvatar ? (
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
{userName?.[0].toLocaleUpperCase()}
</div>
) : (
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
)}
{useCurrentUserAvatar
? (
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
{userName?.[0].toLocaleUpperCase()}
</div>
)
: (
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
)}
</div>
)
}
@@ -411,7 +416,7 @@ const Chat: FC<IChatProps> = ({
controlClearQuery,
controlFocus,
isShowSuggestion,
suggestionList
suggestionList,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
@@ -436,27 +441,24 @@ const Chat: FC<IChatProps> = ({
}
useEffect(() => {
if (controlClearQuery) {
if (controlClearQuery)
setQuery('')
}
}, [controlClearQuery])
const handleSend = () => {
if (!valid() || (checkCanSend && !checkCanSend()))
return
onSend(query)
if (!isResponsing) {
if (!isResponsing)
setQuery('')
}
}
const handleKeyUp = (e: any) => {
if (e.code === 'Enter') {
e.preventDefault()
// prevent send message when using input method enter
if (!e.shiftKey && !isUseInputMethod.current) {
if (!e.shiftKey && !isUseInputMethod.current)
handleSend()
}
}
}
@@ -468,6 +470,10 @@ const Chat: FC<IChatProps> = ({
}
}
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const sendBtn = <div className={cn(!(!query || query.trim() === '') && s.sendBtnActive, `${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`)} onClick={handleSend}></div>
return (
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
{/* Chat List */}
@@ -506,7 +512,7 @@ const Chat: FC<IChatProps> = ({
<div className='flex items-center justify-center mb-2.5'>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)'
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
}}></div>
<div className='shrink-0 flex items-center px-3 space-x-1'>
{TryToAskIcon}
@@ -514,7 +520,7 @@ const Chat: FC<IChatProps> = ({
</div>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)'
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
}}></div>
</div>
<div className='flex justify-center overflow-x-scroll pb-2'>
@@ -544,17 +550,21 @@ const Chat: FC<IChatProps> = ({
/>
<div className="absolute top-0 right-2 flex items-center h-[48px]">
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
<Tooltip
selector='send-tip'
htmlContent={
<div>
<div>{t('common.operation.send')} Enter</div>
<div>{t('common.operation.lineBreak')} Shift Enter</div>
</div>
}
>
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
</Tooltip>
{isMobile
? sendBtn
: (
<Tooltip
selector='send-tip'
htmlContent={
<div>
<div>{t('common.operation.send')} Enter</div>
<div>{t('common.operation.lineBreak')} Shift Enter</div>
</div>
}
>
{sendBtn}
</Tooltip>
)}
</div>
</div>
</div>

View File

@@ -102,6 +102,10 @@
background: url(./icons/send.svg) center center no-repeat;
}
.sendBtnActive {
background-image: url(./icons/send-active.svg);
}
.sendBtn:hover {
background-image: url(./icons/send-active.svg);
background-color: #EBF5FF;

View File

@@ -12,7 +12,6 @@ import { formatNumber } from '@/utils/format'
import Link from 'next/link'
import s from './style.module.css'
import Toast from '@/app/components/base/toast'
export interface ISelectDataSetProps {
isShow: boolean
@@ -32,8 +31,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
const [loaded, setLoaded] = React.useState(false)
const [datasets, setDataSets] = React.useState<DataSet[] | null>(null)
const hasNoData = !datasets || datasets?.length === 0
// Only one dataset can be selected. Historical data retains data and supports multiple selections, but when saving, only one can be selected. This is based on considerations of performance and accuracy.
const canSelectMulti = selectedIds.length > 1
const canSelectMulti = true
useEffect(() => {
(async () => {
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
@@ -57,13 +55,6 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}
const handleSelect = () => {
if (selected.length > 1) {
Toast.notify({
type: 'error',
message: t('appDebug.feature.dataSet.notSupportSelectMulti')
})
return
}
onSelect(selected)
}
return (

View File

@@ -56,10 +56,11 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
}, [value])
const coloredContent = (tempValue || '')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
const handleEdit = () => {

View File

@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
}}
onClick={onClick}
>
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />}
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
</span>
)
}

View File

@@ -68,10 +68,11 @@ const BlockInput: FC<IBlockInputProps> = ({
})
const coloredContent = (currentValue || '')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
// Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => {

View File

@@ -28,7 +28,7 @@ const CreateAppModal = ({
const [name, setName] = React.useState('')
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const submit = () => {
if(!name.trim()) {
@@ -74,7 +74,7 @@ const CreateAppModal = ({
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}

View File

@@ -5,6 +5,7 @@ import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common'
import { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
export interface IExploreProps {
children: React.ReactNode
@@ -13,12 +14,14 @@ export interface IExploreProps {
const Explore: FC<IExploreProps> = ({
children
}) => {
const { t } = useTranslation()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
const { userProfile } = useAppContext()
const [hasEditPermission, setHasEditPermission] = useState(false)
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
if(!accounts) return

View File

@@ -67,7 +67,7 @@ export default function AccountAbout({
<div className='flex items-center'>
<Link
className={classNames(buttonClassName, 'mr-2')}
href={'https://github.com/langgenius'}
href={'https://github.com/langgenius/dify/releases'}
target='_blank'
>
{t('common.about.changeLog')}

View File

@@ -71,14 +71,14 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
</div>
</div>
<div className='flex items-center'>
{/* <Link href="/explore/apps" className={classNames(
<Link href="/explore/apps" className={classNames(
navClassName, 'group',
isExplore && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
)}>
<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.explore')}
</Link> */}
</Link>
<Nav
icon={<BuildAppsIcon isSelected={['apps', 'app'].includes(selectedSegment || '')} />}
text={t('common.menus.apps')}

View File

@@ -60,8 +60,9 @@ const ConfigSence: FC<IConfigSenceProps> = ({
</div>
</div>
))}
<div className='mt-6 h-[1px] bg-gray-100'></div>
{promptConfig.prompt_variables.length > 0 && (
<div className='mt-6 h-[1px] bg-gray-100'></div>
)}
<div className='w-full mt-5'>
<label className='text-gray-900 text-sm font-medium'>{t('share.generation.queryTitle')}</label>
<div className="mt-2 overflow-hidden rounded-lg bg-gray-50 ">

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from 'classnames'
import { useBoolean, useClickAway } from 'ahooks'
import { useContext } from 'use-context-selector'
import ConfigScence from '@/app/components/share/text-generation/config-scence'
import NoData from '@/app/components/share/text-generation/no-data'
// import History from '@/app/components/share/text-generation/history'
@@ -12,6 +11,7 @@ import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, sa
import type { SiteInfo } from '@/models/share'
import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { Feedbacktype } from '@/app/components/app/chat'
import { changeLanguage } from '@/i18n/i18next-config'
import Loading from '@/app/components/base/loading'
@@ -24,6 +24,7 @@ import s from './style.module.css'
import Button from '../../base/button'
import { App } from '@/types/app'
import { InstalledApp } from '@/models/explore'
import { appDefaultIconBackground } from '@/config'
export type IMainProps = {
isInstalledApp?: boolean,
@@ -283,7 +284,7 @@ const TextGeneration: FC<IMainProps> = ({
<div className='mb-6'>
<div className='flex justify-between items-center'>
<div className='flex items-center space-x-3'>
<div className={cn(s.appIcon, 'shrink-0')}></div>
<AppIcon size="small" icon={siteInfo.icon} background={siteInfo.icon_background || appDefaultIconBackground} />
<div className='text-lg text-gray-800 font-semibold'>{siteInfo.title}</div>
</div>
{!isPC && (

View File

@@ -4,13 +4,6 @@
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}
.appIcon {
width: 32px;
height: 32px;
background: url(./icons/app-icon.svg) center center no-repeat;
background-size: contain;
}
.starIcon {
width: 16px;
height: 16px;

View File

@@ -779,7 +779,7 @@
max-width: 100%;
padding: 0;
margin: 0;
overflow-x: scroll;
overflow-x: auto;
line-height: inherit;
word-wrap: normal;
background-color: transparent;

View File

@@ -1,4 +1,5 @@
const translation = {
title: 'Apps',
createApp: 'Create new App',
modes: {
completion: 'Text Generator',

View File

@@ -1,4 +1,5 @@
const translation = {
title: '应用',
createApp: '创建应用',
modes: {
completion: '文本生成型',

View File

@@ -82,11 +82,11 @@ const translation = {
settings: {
accountGroup: 'ACCOUNT',
workplaceGroup: 'WORKPLACE',
account: "My account",
members: "Members",
integrations: "Integrations",
language: "Language",
provider: "Model Provider"
account: 'My account',
members: 'Members',
integrations: 'Integrations',
language: 'Language',
provider: 'Model Provider',
},
account: {
avatar: 'Avatar',
@@ -99,7 +99,7 @@ const translation = {
},
members: {
team: 'Team',
invite: 'Invite',
invite: 'Add',
name: 'NAME',
lastActive: 'LAST ACTIVE',
role: 'ROLES',
@@ -109,14 +109,14 @@ const translation = {
adminTip: 'Can build apps & manage team settings',
normal: 'Normal',
normalTip: 'Only can use appscan not build apps',
inviteTeamMember: 'Invite team member',
inviteTeamMemberTip: 'The other person will receive an email. If he\'s already a Dify user, he can access your team data directly after signing in.',
inviteTeamMember: 'Add team member',
inviteTeamMemberTip: 'He can access your team data directly after signing in.',
email: 'Email',
emailInvalid: 'Invalid Email Format',
emailPlaceholder: 'Input Email',
sendInvite: 'Send Invite',
invitationSent: 'Invitation sent',
invitationSentTip: 'The invitation is sent, and they can sign in to Dify to access your team data.',
sendInvite: 'Add',
invitationSent: 'Added',
invitationSentTip: 'Added, and they can sign in to Dify to access your team data.',
ok: 'OK',
removeFromTeam: 'Remove from team',
removeFromTeamTip: 'Will remove team access',
@@ -132,20 +132,20 @@ const translation = {
googleAccount: 'Login with Google account',
github: 'GitHub',
githubAccount: 'Login with GitHub account',
connect: 'Connect'
connect: 'Connect',
},
language: {
displayLanguage: 'Display Language',
timezone: 'Time Zone',
},
provider: {
apiKey: "API Key",
enterYourKey: "Enter your API key here",
invalidKey: "Invalid OpenAI API key",
validatedError: "Validation failed: ",
validating: "Validating key...",
saveFailed: "Save api key failed",
apiKeyExceedBill: "This API KEY has no quota available, please read",
apiKey: 'API Key',
enterYourKey: 'Enter your API key here',
invalidKey: 'Invalid OpenAI API key',
validatedError: 'Validation failed: ',
validating: 'Validating key...',
saveFailed: 'Save api key failed',
apiKeyExceedBill: 'This API KEY has no quota available, please read',
addKey: 'Add Key',
comingSoon: 'Coming Soon',
editKey: 'Edit',
@@ -170,7 +170,7 @@ const translation = {
encrypted: {
front: 'Your API KEY will be encrypted and stored using',
back: ' technology.',
}
},
},
about: {
changeLog: 'Changlog',

View File

@@ -50,7 +50,7 @@ const translation = {
'Frequency penalty 是根据重复词在目前文本中的出现频率来对其进行惩罚。正值将不太可能重复常用单词和短语。',
maxToken: '最大 Token',
maxTokenTip:
'生成的最大令牌数为 2,048 或 4,000取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 4 个中文字符。',
'生成的最大令牌数为 2,048 或 4,000取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 个中文字符。',
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 4,000。',
},
tone: {
@@ -82,11 +82,11 @@ const translation = {
settings: {
accountGroup: '账户',
workplaceGroup: '工作空间',
account: "我的账户",
members: "成员",
integrations: "集成",
language: "语言",
provider: "模型供应商"
account: '我的账户',
members: '成员',
integrations: '集成',
language: '语言',
provider: '模型供应商',
},
account: {
avatar: '头像',
@@ -100,7 +100,7 @@ const translation = {
},
members: {
team: '团队',
invite: '邀请',
invite: '添加',
name: '姓名',
lastActive: '上次活动时间',
role: '角色',
@@ -110,14 +110,14 @@ const translation = {
adminTip: '能够建立应用程序和管理团队设置',
normal: '正常人',
normalTip: '只能使用应用程序,不能建立应用程序',
inviteTeamMember: '邀请团队成员',
inviteTeamMemberTip: '对方会收到一封邮件。如果他已经是 Dify 用户则可直接在登录后访问你的团队数据。',
inviteTeamMember: '添加团队成员',
inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
email: '邮箱',
emailInvalid: '邮箱格式无效',
emailPlaceholder: '输入邮箱',
sendInvite: '发送邀请',
invitationSent: '邀请已发送',
invitationSentTip: '邀请已发送,对方登录 Dify 后即可访问你的团队数据。',
sendInvite: '添加',
invitationSent: '已添加',
invitationSentTip: '已添加,对方登录 Dify 后即可访问你的团队数据。',
ok: '好的',
removeFromTeam: '移除团队',
removeFromTeamTip: '将取消团队访问',
@@ -133,20 +133,20 @@ const translation = {
googleAccount: 'Google 账号登录',
github: 'GitHub',
githubAccount: 'GitHub 账号登录',
connect: '绑定'
connect: '绑定',
},
language: {
displayLanguage: '界面语言',
timezone: '时区',
},
provider: {
apiKey: "API 密钥",
enterYourKey: "输入你的 API 密钥",
apiKey: 'API 密钥',
enterYourKey: '输入你的 API 密钥',
invalidKey: '无效的 OpenAI API 密钥',
validatedError: "校验失败:",
validating: "验证密钥中...",
saveFailed: "API 密钥保存失败",
apiKeyExceedBill: "此 API KEY 已没有可用配额,请阅读",
validatedError: '校验失败:',
validating: '验证密钥中...',
saveFailed: 'API 密钥保存失败',
apiKeyExceedBill: '此 API KEY 已没有可用配额,请阅读',
addKey: '添加 密钥',
comingSoon: '即将推出',
editKey: '编辑',
@@ -171,7 +171,7 @@ const translation = {
encrypted: {
front: '密钥将使用 ',
back: ' 技术进行加密和存储。',
}
},
},
about: {
changeLog: '更新日志',

View File

@@ -9,7 +9,7 @@ const translation = {
'删除数据集是不可逆的。用户将无法再访问您的数据集,所有的提示配置和日志将被永久删除。',
datasetDeleted: '数据集已删除',
datasetDeleteFailed: '删除数据集失败',
didYouKnow: '你知道吗??',
didYouKnow: '你知道吗?',
intro1: '数据集可以被集成到 Dify 应用中',
intro2: '作为上下文',
intro3: ',',

View File

@@ -1,4 +1,5 @@
const translation = {
title: 'My Apps',
sidebar: {
discovery: 'Discovery',
workspace: 'Workspace',

View File

@@ -1,4 +1,5 @@
const translation = {
title: '我的应用',
sidebar: {
discovery: '发现',
workspace: '工作区',

View File

@@ -23,11 +23,12 @@ export const getLocale = (request: NextRequest): Locale => {
}
// match locale
let matchedLocale:Locale = i18n.defaultLocale
let matchedLocale: Locale = i18n.defaultLocale
try {
// If languages is ['*'], Error would happen in match function.
matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
} catch(e) {}
}
catch (e) {}
return matchedLocale
}

View File

@@ -1,3 +1,5 @@
const { withSentryConfig } = require('@sentry/nextjs')
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
@@ -29,6 +31,9 @@ const nextConfig = {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
ignoreBuildErrors: true,
},
sentry: {
hideSourceMaps: true,
},
async redirects() {
return [
{
@@ -40,4 +45,17 @@ const nextConfig = {
},
}
module.exports = withMDX(nextConfig)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup
const sentryWebpackPluginOptions = {
org: 'perfectworld',
project: 'javascript-nextjs',
silent: true, // Suppresses all logs
sourcemaps: {
assets: './**',
ignore: ['./node_modules/**'],
},
// https://github.com/getsentry/sentry-webpack-plugin#options.
}
module.exports = withMDX(withSentryConfig(nextConfig, sentryWebpackPluginOptions))

View File

@@ -1,13 +1,15 @@
{
"name": "dify-web",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"fix": "next lint --fix"
"fix": "next lint --fix",
"eslint-fix": "eslint --fix",
"prepare": "cd ../ && husky install ./web/.husky"
},
"dependencies": {
"@emoji-mart/data": "^1.1.2",
@@ -17,6 +19,7 @@
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.2.4",
"@sentry/nextjs": "^7.53.1",
"@tailwindcss/line-clamp": "^0.4.2",
"@types/crypto-js": "^4.1.1",
"@types/lodash-es": "^4.17.7",
@@ -77,8 +80,18 @@
"@types/qs": "^6.9.7",
"autoprefixer": "^10.4.14",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"miragejs": "^0.1.47",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7"
},
"lint-staged": {
"**/*.js?(x)": [
"eslint --fix"
],
"**/*.ts?(x)": [
"eslint --fix"
]
}
}

View File

@@ -0,0 +1,23 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Replay may only be enabled for the client-side
integrations: [new Sentry.Replay()],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
})

16
web/sentry.edge.config.js Normal file
View File

@@ -0,0 +1,16 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
})

View File

@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
})

View File

@@ -50,21 +50,20 @@ module.exports = {
indigo: {
25: '#F5F8FF',
100: '#E0EAFF',
600: '#444CE7'
}
600: '#444CE7',
},
},
screens: {
'mobile': '100px',
mobile: '100px',
// => @media (min-width: 100px) { ... }
'tablet': '640px', // 391
tablet: '640px', // 391
// => @media (min-width: 600px) { ... }
'pc': '769px',
pc: '769px',
// => @media (min-width: 769px) { ... }
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'),
],
}