mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-05 22:55:58 +00:00
Add classifier settings
This commit is contained in:
parent
53fdb100ab
commit
8c4f2e702b
@ -15,7 +15,9 @@ trait OCollective[F[_]] {
|
|||||||
|
|
||||||
def find(name: Ident): F[Option[RCollective]]
|
def find(name: Ident): F[Option[RCollective]]
|
||||||
|
|
||||||
def updateSettings(collective: Ident, lang: OCollective.Settings): F[AddResult]
|
def updateSettings(collective: Ident, settings: OCollective.Settings): F[AddResult]
|
||||||
|
|
||||||
|
def findSettings(collective: Ident): F[Option[OCollective.Settings]]
|
||||||
|
|
||||||
def listUser(collective: Ident): F[Vector[RUser]]
|
def listUser(collective: Ident): F[Vector[RUser]]
|
||||||
|
|
||||||
@ -55,6 +57,8 @@ object OCollective {
|
|||||||
|
|
||||||
type Settings = RCollective.Settings
|
type Settings = RCollective.Settings
|
||||||
val Settings = RCollective.Settings
|
val Settings = RCollective.Settings
|
||||||
|
type Classifier = RClassifierSetting.Classifier
|
||||||
|
val Classifier = RClassifierSetting.Classifier
|
||||||
|
|
||||||
sealed trait PassChangeResult
|
sealed trait PassChangeResult
|
||||||
object PassChangeResult {
|
object PassChangeResult {
|
||||||
@ -102,6 +106,9 @@ object OCollective {
|
|||||||
.attempt
|
.attempt
|
||||||
.map(AddResult.fromUpdate)
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
|
def findSettings(collective: Ident): F[Option[OCollective.Settings]] =
|
||||||
|
store.transact(RCollective.getSettings(collective))
|
||||||
|
|
||||||
def listUser(collective: Ident): F[Vector[RUser]] =
|
def listUser(collective: Ident): F[Vector[RUser]] =
|
||||||
store.transact(RUser.findAll(collective, _.login))
|
store.transact(RUser.findAll(collective, _.login))
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package docspell.common
|
||||||
|
|
||||||
|
import docspell.common.syntax.all._
|
||||||
|
|
||||||
|
import io.circe._
|
||||||
|
import io.circe.generic.semiauto._
|
||||||
|
|
||||||
|
/** Arguments to the classify-item task.
|
||||||
|
*
|
||||||
|
* This task is run periodically and learns from existing documents
|
||||||
|
* to create a model for predicting tags of new documents. The user
|
||||||
|
* must give a tag category as a subset of possible tags..
|
||||||
|
*/
|
||||||
|
case class LearnClassifierArgs(
|
||||||
|
collective: Ident
|
||||||
|
) {
|
||||||
|
|
||||||
|
def makeSubject: String =
|
||||||
|
"Learn tags"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object LearnClassifierArgs {
|
||||||
|
|
||||||
|
val taskName = Ident.unsafe("learn-classifier")
|
||||||
|
|
||||||
|
implicit val jsonEncoder: Encoder[LearnClassifierArgs] =
|
||||||
|
deriveEncoder[LearnClassifierArgs]
|
||||||
|
implicit val jsonDecoder: Decoder[LearnClassifierArgs] =
|
||||||
|
deriveDecoder[LearnClassifierArgs]
|
||||||
|
|
||||||
|
def parse(str: String): Either[Throwable, LearnClassifierArgs] =
|
||||||
|
str.parseJsonAs[LearnClassifierArgs]
|
||||||
|
|
||||||
|
}
|
@ -271,6 +271,50 @@ docspell.joex {
|
|||||||
# file will be kept until a check for a state change is done.
|
# file will be kept until a check for a state change is done.
|
||||||
file-cache-time = "1 minute"
|
file-cache-time = "1 minute"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Settings for doing document classification.
|
||||||
|
#
|
||||||
|
# This works by learning from existing documents. A collective can
|
||||||
|
# specify a tag category and the system will try to predict a tag
|
||||||
|
# from this category for new incoming documents.
|
||||||
|
#
|
||||||
|
# This requires a satstical model that is computed from all
|
||||||
|
# existing documents. This process is run periodically as
|
||||||
|
# configured by the collective. It may require a lot of memory,
|
||||||
|
# depending on the amount of data.
|
||||||
|
#
|
||||||
|
# It utilises this NLP library: https://nlp.stanford.edu/.
|
||||||
|
classification {
|
||||||
|
# Whether to enable classification globally. Each collective can
|
||||||
|
# decide to disable it. If it is disabled here, no collective
|
||||||
|
# can use classification.
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# If concerned with memory consumption, this restricts the
|
||||||
|
# number of items to consider. More are better for training. A
|
||||||
|
# negative value or zero means no train on all items.
|
||||||
|
item-count = 0
|
||||||
|
|
||||||
|
# These settings are used to configure the classifier. If
|
||||||
|
# multiple are given, they are all tried and the "best" is
|
||||||
|
# chosen at the end. See
|
||||||
|
# https://nlp.stanford.edu/wiki/Software/Classifier/20_Newsgroups
|
||||||
|
# for more info about these settings. The settings are almost
|
||||||
|
# identical to them, as they yielded best results with *my*
|
||||||
|
# dataset.
|
||||||
|
#
|
||||||
|
# Enclose regexps in triple quotes.
|
||||||
|
classifiers = [
|
||||||
|
{ "useSplitWords" = "true"
|
||||||
|
"splitWordsTokenizerRegexp" = """[\p{L}][\p{L}0-9]*|(?:\$ ?)?[0-9]+(?:\.[0-9]{2})?%?|\s+|."""
|
||||||
|
"splitWordsIgnoreRegexp" = """\s+"""
|
||||||
|
"useSplitPrefixSuffixNGrams" = "true"
|
||||||
|
"maxNGramLeng" = "4"
|
||||||
|
"minNGramLeng" = "1"
|
||||||
|
"splitWordShape" = "chris4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration for converting files into PDFs.
|
# Configuration for converting files into PDFs.
|
||||||
|
@ -57,7 +57,8 @@ object Config {
|
|||||||
case class TextAnalysis(
|
case class TextAnalysis(
|
||||||
maxLength: Int,
|
maxLength: Int,
|
||||||
workingDir: Path,
|
workingDir: Path,
|
||||||
regexNer: RegexNer
|
regexNer: RegexNer,
|
||||||
|
classification: Classification
|
||||||
) {
|
) {
|
||||||
|
|
||||||
def textAnalysisConfig: TextAnalysisConfig =
|
def textAnalysisConfig: TextAnalysisConfig =
|
||||||
@ -68,4 +69,10 @@ object Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case class RegexNer(enabled: Boolean, fileCacheTime: Duration)
|
case class RegexNer(enabled: Boolean, fileCacheTime: Duration)
|
||||||
|
|
||||||
|
case class Classification(
|
||||||
|
enabled: Boolean,
|
||||||
|
itemCount: Int,
|
||||||
|
classifiers: List[Map[String, String]]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -3643,12 +3643,14 @@ components:
|
|||||||
description: DateTime
|
description: DateTime
|
||||||
type: integer
|
type: integer
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
||||||
CollectiveSettings:
|
CollectiveSettings:
|
||||||
description: |
|
description: |
|
||||||
Settings for a collective.
|
Settings for a collective.
|
||||||
required:
|
required:
|
||||||
- language
|
- language
|
||||||
- integrationEnabled
|
- integrationEnabled
|
||||||
|
- classifier
|
||||||
properties:
|
properties:
|
||||||
language:
|
language:
|
||||||
type: string
|
type: string
|
||||||
@ -3658,6 +3660,31 @@ components:
|
|||||||
description: |
|
description: |
|
||||||
Whether the collective has the integration endpoint
|
Whether the collective has the integration endpoint
|
||||||
enabled.
|
enabled.
|
||||||
|
classifier:
|
||||||
|
$ref: "#/components/schemas/ClassifierSetting"
|
||||||
|
|
||||||
|
ClassifierSetting:
|
||||||
|
description: |
|
||||||
|
Settings for learning a document classifier.
|
||||||
|
required:
|
||||||
|
- enabled
|
||||||
|
- schedule
|
||||||
|
- itemCount
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
category:
|
||||||
|
type: string
|
||||||
|
itemCount:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: |
|
||||||
|
The max. number of items to learn from. The newest items
|
||||||
|
are considered.
|
||||||
|
schedule:
|
||||||
|
type: string
|
||||||
|
format: calevent
|
||||||
|
|
||||||
SourceList:
|
SourceList:
|
||||||
description: |
|
description: |
|
||||||
A list of sources.
|
A list of sources.
|
||||||
|
@ -10,6 +10,7 @@ import docspell.restapi.model._
|
|||||||
import docspell.restserver.conv.Conversions
|
import docspell.restserver.conv.Conversions
|
||||||
import docspell.restserver.http4s._
|
import docspell.restserver.http4s._
|
||||||
|
|
||||||
|
import com.github.eikek.calev.CalEvent
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import org.http4s.circe.CirceEntityDecoder._
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
@ -37,7 +38,18 @@ object CollectiveRoutes {
|
|||||||
case req @ POST -> Root / "settings" =>
|
case req @ POST -> Root / "settings" =>
|
||||||
for {
|
for {
|
||||||
settings <- req.as[CollectiveSettings]
|
settings <- req.as[CollectiveSettings]
|
||||||
sett = OCollective.Settings(settings.language, settings.integrationEnabled)
|
sett = OCollective.Settings(
|
||||||
|
settings.language,
|
||||||
|
settings.integrationEnabled,
|
||||||
|
Some(
|
||||||
|
OCollective.Classifier(
|
||||||
|
settings.classifier.enabled,
|
||||||
|
settings.classifier.schedule,
|
||||||
|
settings.classifier.itemCount,
|
||||||
|
settings.classifier.category
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
res <-
|
res <-
|
||||||
backend.collective
|
backend.collective
|
||||||
.updateSettings(user.account.collective, sett)
|
.updateSettings(user.account.collective, sett)
|
||||||
@ -46,8 +58,21 @@ object CollectiveRoutes {
|
|||||||
|
|
||||||
case GET -> Root / "settings" =>
|
case GET -> Root / "settings" =>
|
||||||
for {
|
for {
|
||||||
collDb <- backend.collective.find(user.account.collective)
|
settDb <- backend.collective.findSettings(user.account.collective)
|
||||||
sett = collDb.map(c => CollectiveSettings(c.language, c.integrationEnabled))
|
sett = settDb.map(c =>
|
||||||
|
CollectiveSettings(
|
||||||
|
c.language,
|
||||||
|
c.integrationEnabled,
|
||||||
|
ClassifierSetting(
|
||||||
|
c.classifier.map(_.enabled).getOrElse(false),
|
||||||
|
c.classifier.flatMap(_.category),
|
||||||
|
c.classifier.map(_.itemCount).getOrElse(0),
|
||||||
|
c.classifier
|
||||||
|
.map(_.schedule)
|
||||||
|
.getOrElse(CalEvent.unsafe("*-1/3-01 01:00:00"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
resp <- sett.toResponse()
|
resp <- sett.toResponse()
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE `classifier_setting` (
|
||||||
|
`cid` varchar(254) not null primary key,
|
||||||
|
`enabled` boolean not null,
|
||||||
|
`schedule` varchar(254) not null,
|
||||||
|
`category` varchar(254) not null,
|
||||||
|
`file_id` varchar(254),
|
||||||
|
`created` timestamp not null,
|
||||||
|
foreign key (`cid`) references `collective`(`cid`)
|
||||||
|
);
|
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE "classifier_setting" (
|
||||||
|
"cid" varchar(254) not null primary key,
|
||||||
|
"enabled" boolean not null,
|
||||||
|
"schedule" varchar(254) not null,
|
||||||
|
"category" varchar(254) not null,
|
||||||
|
"item_count" int not null,
|
||||||
|
"file_id" varchar(254),
|
||||||
|
"created" timestamp not null,
|
||||||
|
foreign key ("cid") references "collective"("cid"),
|
||||||
|
foreign key ("file_id") references "filemeta"("id")
|
||||||
|
);
|
@ -0,0 +1,106 @@
|
|||||||
|
package docspell.store.records
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store.impl.Implicits._
|
||||||
|
import docspell.store.impl._
|
||||||
|
|
||||||
|
import com.github.eikek.calev._
|
||||||
|
import doobie._
|
||||||
|
import doobie.implicits._
|
||||||
|
|
||||||
|
case class RClassifierSetting(
|
||||||
|
cid: Ident,
|
||||||
|
enabled: Boolean,
|
||||||
|
schedule: CalEvent,
|
||||||
|
category: String,
|
||||||
|
itemCount: Int,
|
||||||
|
fileId: Option[Ident],
|
||||||
|
created: Timestamp
|
||||||
|
) {}
|
||||||
|
|
||||||
|
object RClassifierSetting {
|
||||||
|
|
||||||
|
val table = fr"classifier_setting"
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
val cid = Column("cid")
|
||||||
|
val enabled = Column("enabled")
|
||||||
|
val schedule = Column("schedule")
|
||||||
|
val category = Column("category")
|
||||||
|
val itemCount = Column("item_count")
|
||||||
|
val fileId = Column("file_id")
|
||||||
|
val created = Column("created")
|
||||||
|
val all = List(cid, enabled, schedule, category, itemCount, fileId, created)
|
||||||
|
}
|
||||||
|
import Columns._
|
||||||
|
|
||||||
|
def insert(v: RClassifierSetting): ConnectionIO[Int] = {
|
||||||
|
val sql =
|
||||||
|
insertRow(
|
||||||
|
table,
|
||||||
|
all,
|
||||||
|
fr"${v.cid},${v.enabled},${v.schedule},${v.category},${v.itemCount},${v.fileId},${v.created}"
|
||||||
|
)
|
||||||
|
sql.update.run
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateAll(v: RClassifierSetting): ConnectionIO[Int] = {
|
||||||
|
val sql = updateRow(
|
||||||
|
table,
|
||||||
|
cid.is(v.cid),
|
||||||
|
commas(
|
||||||
|
enabled.setTo(v.enabled),
|
||||||
|
schedule.setTo(v.schedule),
|
||||||
|
category.setTo(v.category),
|
||||||
|
itemCount.setTo(v.itemCount),
|
||||||
|
fileId.setTo(v.fileId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sql.update.run
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateSettings(v: RClassifierSetting): ConnectionIO[Int] =
|
||||||
|
for {
|
||||||
|
n1 <- updateRow(
|
||||||
|
table,
|
||||||
|
cid.is(v.cid),
|
||||||
|
commas(
|
||||||
|
enabled.setTo(v.enabled),
|
||||||
|
schedule.setTo(v.schedule),
|
||||||
|
itemCount.setTo(v.itemCount),
|
||||||
|
category.setTo(v.category)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
|
||||||
|
} yield n1 + n2
|
||||||
|
|
||||||
|
def findById(id: Ident): ConnectionIO[Option[RClassifierSetting]] = {
|
||||||
|
val sql = selectSimple(all, table, cid.is(id))
|
||||||
|
sql.query[RClassifierSetting].option
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete(coll: Ident): ConnectionIO[Int] =
|
||||||
|
deleteFrom(table, cid.is(coll)).update.run
|
||||||
|
|
||||||
|
case class Classifier(
|
||||||
|
enabled: Boolean,
|
||||||
|
schedule: CalEvent,
|
||||||
|
itemCount: Int,
|
||||||
|
category: Option[String]
|
||||||
|
) {
|
||||||
|
|
||||||
|
def toRecord(coll: Ident, created: Timestamp): RClassifierSetting =
|
||||||
|
RClassifierSetting(
|
||||||
|
coll,
|
||||||
|
enabled,
|
||||||
|
schedule,
|
||||||
|
category.getOrElse(""),
|
||||||
|
itemCount,
|
||||||
|
None,
|
||||||
|
created
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -61,14 +61,47 @@ object RCollective {
|
|||||||
updateRow(table, id.is(cid), language.setTo(lang)).update.run
|
updateRow(table, id.is(cid), language.setTo(lang)).update.run
|
||||||
|
|
||||||
def updateSettings(cid: Ident, settings: Settings): ConnectionIO[Int] =
|
def updateSettings(cid: Ident, settings: Settings): ConnectionIO[Int] =
|
||||||
updateRow(
|
for {
|
||||||
table,
|
n1 <- updateRow(
|
||||||
id.is(cid),
|
table,
|
||||||
commas(
|
id.is(cid),
|
||||||
language.setTo(settings.language),
|
commas(
|
||||||
integration.setTo(settings.integrationEnabled)
|
language.setTo(settings.language),
|
||||||
)
|
integration.setTo(settings.integrationEnabled)
|
||||||
).update.run
|
)
|
||||||
|
).update.run
|
||||||
|
cls <-
|
||||||
|
Timestamp
|
||||||
|
.current[ConnectionIO]
|
||||||
|
.map(now => settings.classifier.map(_.toRecord(cid, now)))
|
||||||
|
n2 <- cls match {
|
||||||
|
case Some(cr) =>
|
||||||
|
RClassifierSetting.updateSettings(cr)
|
||||||
|
case None =>
|
||||||
|
RClassifierSetting.delete(cid)
|
||||||
|
}
|
||||||
|
} yield n1 + n2
|
||||||
|
|
||||||
|
def getSettings(coll: Ident): ConnectionIO[Option[Settings]] = {
|
||||||
|
val cId = id.prefix("c")
|
||||||
|
val CS = RClassifierSetting.Columns
|
||||||
|
val csCid = CS.cid.prefix("cs")
|
||||||
|
|
||||||
|
val cols = Seq(
|
||||||
|
language.prefix("c"),
|
||||||
|
integration.prefix("c"),
|
||||||
|
CS.enabled.prefix("cs"),
|
||||||
|
CS.schedule.prefix("cs"),
|
||||||
|
CS.itemCount.prefix("cs"),
|
||||||
|
CS.category.prefix("cs")
|
||||||
|
)
|
||||||
|
val from = table ++ fr"c LEFT JOIN" ++
|
||||||
|
RClassifierSetting.table ++ fr"cs ON" ++ csCid.is(cId)
|
||||||
|
|
||||||
|
selectSimple(cols, from, cId.is(coll))
|
||||||
|
.query[Settings]
|
||||||
|
.option
|
||||||
|
}
|
||||||
|
|
||||||
def findById(cid: Ident): ConnectionIO[Option[RCollective]] = {
|
def findById(cid: Ident): ConnectionIO[Option[RCollective]] = {
|
||||||
val sql = selectSimple(all, table, id.is(cid))
|
val sql = selectSimple(all, table, id.is(cid))
|
||||||
@ -112,5 +145,10 @@ object RCollective {
|
|||||||
selectSimple(all.map(_.prefix("c")), from, aId.is(attachId)).query[RCollective].option
|
selectSimple(all.map(_.prefix("c")), from, aId.is(attachId)).query[RCollective].option
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Settings(language: Language, integrationEnabled: Boolean)
|
case class Settings(
|
||||||
|
language: Language,
|
||||||
|
integrationEnabled: Boolean,
|
||||||
|
classifier: Option[RClassifierSetting.Classifier]
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -218,12 +218,12 @@ loginInfo model =
|
|||||||
, menuEntry model
|
, menuEntry model
|
||||||
CollectiveSettingPage
|
CollectiveSettingPage
|
||||||
[ i [ class "users circle icon" ] []
|
[ i [ class "users circle icon" ] []
|
||||||
, text "Collective Settings"
|
, text "Collective Profile"
|
||||||
]
|
]
|
||||||
, menuEntry model
|
, menuEntry model
|
||||||
UserSettingPage
|
UserSettingPage
|
||||||
[ i [ class "user circle icon" ] []
|
[ i [ class "user circle icon" ] []
|
||||||
, text "User Settings"
|
, text "User Profile"
|
||||||
]
|
]
|
||||||
, div [ class "divider" ] []
|
, div [ class "divider" ] []
|
||||||
, menuEntry model
|
, menuEntry model
|
||||||
|
199
modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm
Normal file
199
modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
module Comp.ClassifierSettingsForm exposing
|
||||||
|
( Model
|
||||||
|
, Msg
|
||||||
|
, getSettings
|
||||||
|
, init
|
||||||
|
, update
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api
|
||||||
|
import Api.Model.ClassifierSetting exposing (ClassifierSetting)
|
||||||
|
import Api.Model.TagList exposing (TagList)
|
||||||
|
import Comp.CalEventInput
|
||||||
|
import Comp.FixedDropdown
|
||||||
|
import Comp.IntField
|
||||||
|
import Data.CalEvent exposing (CalEvent)
|
||||||
|
import Data.Flags exposing (Flags)
|
||||||
|
import Data.Validated exposing (Validated(..))
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (onCheck)
|
||||||
|
import Http
|
||||||
|
import Util.Tag
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ enabled : Bool
|
||||||
|
, categoryModel : Comp.FixedDropdown.Model String
|
||||||
|
, category : Maybe String
|
||||||
|
, scheduleModel : Comp.CalEventInput.Model
|
||||||
|
, schedule : Validated CalEvent
|
||||||
|
, itemCountModel : Comp.IntField.Model
|
||||||
|
, itemCount : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= GetTagsResp (Result Http.Error TagList)
|
||||||
|
| ScheduleMsg Comp.CalEventInput.Msg
|
||||||
|
| ToggleEnabled
|
||||||
|
| CategoryMsg (Comp.FixedDropdown.Msg String)
|
||||||
|
| ItemCountMsg Comp.IntField.Msg
|
||||||
|
|
||||||
|
|
||||||
|
init : Flags -> ClassifierSetting -> ( Model, Cmd Msg )
|
||||||
|
init flags sett =
|
||||||
|
let
|
||||||
|
newSchedule =
|
||||||
|
Data.CalEvent.fromEvent sett.schedule
|
||||||
|
|> Maybe.withDefault Data.CalEvent.everyMonth
|
||||||
|
|
||||||
|
( cem, cec ) =
|
||||||
|
Comp.CalEventInput.init flags newSchedule
|
||||||
|
in
|
||||||
|
( { enabled = sett.enabled
|
||||||
|
, categoryModel = Comp.FixedDropdown.initString []
|
||||||
|
, category = Nothing
|
||||||
|
, scheduleModel = cem
|
||||||
|
, schedule = Data.Validated.Unknown newSchedule
|
||||||
|
, itemCountModel = Comp.IntField.init (Just 0) Nothing True "Item Count"
|
||||||
|
, itemCount = Just sett.itemCount
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.getTags flags "" GetTagsResp
|
||||||
|
, Cmd.map ScheduleMsg cec
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
getSettings : Model -> Validated ClassifierSetting
|
||||||
|
getSettings model =
|
||||||
|
Data.Validated.map
|
||||||
|
(\sch ->
|
||||||
|
{ enabled = model.enabled
|
||||||
|
, category = model.category
|
||||||
|
, schedule =
|
||||||
|
Data.CalEvent.makeEvent sch
|
||||||
|
, itemCount = Maybe.withDefault 0 model.itemCount
|
||||||
|
}
|
||||||
|
)
|
||||||
|
model.schedule
|
||||||
|
|
||||||
|
|
||||||
|
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update flags msg model =
|
||||||
|
case msg of
|
||||||
|
GetTagsResp (Ok tl) ->
|
||||||
|
let
|
||||||
|
categories =
|
||||||
|
Util.Tag.getCategories tl.items
|
||||||
|
|> List.sort
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| categoryModel = Comp.FixedDropdown.initString categories
|
||||||
|
, category = List.head categories
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
GetTagsResp (Err _) ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
ScheduleMsg lmsg ->
|
||||||
|
let
|
||||||
|
( cm, cc, ce ) =
|
||||||
|
Comp.CalEventInput.update
|
||||||
|
flags
|
||||||
|
(Data.Validated.value model.schedule)
|
||||||
|
lmsg
|
||||||
|
model.scheduleModel
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| scheduleModel = cm
|
||||||
|
, schedule = ce
|
||||||
|
}
|
||||||
|
, Cmd.map ScheduleMsg cc
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleEnabled ->
|
||||||
|
( { model | enabled = not model.enabled }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
CategoryMsg lmsg ->
|
||||||
|
let
|
||||||
|
( mm, ma ) =
|
||||||
|
Comp.FixedDropdown.update lmsg model.categoryModel
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| categoryModel = mm
|
||||||
|
, category =
|
||||||
|
if ma == Nothing then
|
||||||
|
model.category
|
||||||
|
|
||||||
|
else
|
||||||
|
ma
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemCountMsg lmsg ->
|
||||||
|
let
|
||||||
|
( im, iv ) =
|
||||||
|
Comp.IntField.update lmsg model.itemCountModel
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| itemCountModel = im
|
||||||
|
, itemCount = iv
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view model =
|
||||||
|
div []
|
||||||
|
[ div
|
||||||
|
[ class "field"
|
||||||
|
]
|
||||||
|
[ div [ class "ui checkbox" ]
|
||||||
|
[ input
|
||||||
|
[ type_ "checkbox"
|
||||||
|
, onCheck (\_ -> ToggleEnabled)
|
||||||
|
, checked model.enabled
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, label [] [ text "Enable classification" ]
|
||||||
|
, span [ class "small-info" ]
|
||||||
|
[ text "Disable document classification if not needed."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "ui basic segment" ]
|
||||||
|
[ text "Document classification tries to predict a tag for new incoming documents. This "
|
||||||
|
, text "works by learning from existing documents in order to find common patterns within "
|
||||||
|
, text "the text. The more documents you have correctly tagged, the better. Learning is done "
|
||||||
|
, text "periodically based on a schedule and you need to specify a tag-group that should "
|
||||||
|
, text "be used for learning."
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [] [ text "Category" ]
|
||||||
|
, Html.map CategoryMsg
|
||||||
|
(Comp.FixedDropdown.viewString model.category
|
||||||
|
model.categoryModel
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, Html.map ItemCountMsg
|
||||||
|
(Comp.IntField.viewWithInfo
|
||||||
|
"The maximum number of items to learn from, order by date newest first. Use 0 to mean all."
|
||||||
|
model.itemCount
|
||||||
|
"field"
|
||||||
|
model.itemCountModel
|
||||||
|
)
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [] [ text "Schedule" ]
|
||||||
|
, Html.map ScheduleMsg
|
||||||
|
(Comp.CalEventInput.view "" (Data.Validated.value model.schedule) model.scheduleModel)
|
||||||
|
]
|
||||||
|
]
|
@ -10,10 +10,12 @@ module Comp.CollectiveSettingsForm exposing
|
|||||||
import Api
|
import Api
|
||||||
import Api.Model.BasicResult exposing (BasicResult)
|
import Api.Model.BasicResult exposing (BasicResult)
|
||||||
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||||
|
import Comp.ClassifierSettingsForm
|
||||||
import Comp.Dropdown
|
import Comp.Dropdown
|
||||||
import Data.Flags exposing (Flags)
|
import Data.Flags exposing (Flags)
|
||||||
import Data.Language exposing (Language)
|
import Data.Language exposing (Language)
|
||||||
import Data.UiSettings exposing (UiSettings)
|
import Data.UiSettings exposing (UiSettings)
|
||||||
|
import Data.Validated exposing (Validated)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (onCheck, onClick, onInput)
|
import Html.Events exposing (onCheck, onClick, onInput)
|
||||||
@ -27,44 +29,58 @@ type alias Model =
|
|||||||
, initSettings : CollectiveSettings
|
, initSettings : CollectiveSettings
|
||||||
, fullTextConfirmText : String
|
, fullTextConfirmText : String
|
||||||
, fullTextReIndexResult : Maybe BasicResult
|
, fullTextReIndexResult : Maybe BasicResult
|
||||||
|
, classifierModel : Comp.ClassifierSettingsForm.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : CollectiveSettings -> Model
|
init : Flags -> CollectiveSettings -> ( Model, Cmd Msg )
|
||||||
init settings =
|
init flags settings =
|
||||||
let
|
let
|
||||||
lang =
|
lang =
|
||||||
Data.Language.fromString settings.language
|
Data.Language.fromString settings.language
|
||||||
|> Maybe.withDefault Data.Language.German
|
|> Maybe.withDefault Data.Language.German
|
||||||
|
|
||||||
|
( cm, cc ) =
|
||||||
|
Comp.ClassifierSettingsForm.init flags settings.classifier
|
||||||
in
|
in
|
||||||
{ langModel =
|
( { langModel =
|
||||||
Comp.Dropdown.makeSingleList
|
Comp.Dropdown.makeSingleList
|
||||||
{ makeOption =
|
{ makeOption =
|
||||||
\l ->
|
\l ->
|
||||||
{ value = Data.Language.toIso3 l
|
{ value = Data.Language.toIso3 l
|
||||||
, text = Data.Language.toName l
|
, text = Data.Language.toName l
|
||||||
, additional = ""
|
, additional = ""
|
||||||
}
|
}
|
||||||
, placeholder = ""
|
, placeholder = ""
|
||||||
, options = Data.Language.all
|
, options = Data.Language.all
|
||||||
, selected = Just lang
|
, selected = Just lang
|
||||||
}
|
}
|
||||||
, intEnabled = settings.integrationEnabled
|
, intEnabled = settings.integrationEnabled
|
||||||
, initSettings = settings
|
, initSettings = settings
|
||||||
, fullTextConfirmText = ""
|
, fullTextConfirmText = ""
|
||||||
, fullTextReIndexResult = Nothing
|
, fullTextReIndexResult = Nothing
|
||||||
}
|
, classifierModel = cm
|
||||||
|
}
|
||||||
|
, Cmd.map ClassifierSettingMsg cc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
getSettings : Model -> CollectiveSettings
|
getSettings : Model -> Validated CollectiveSettings
|
||||||
getSettings model =
|
getSettings model =
|
||||||
CollectiveSettings
|
Data.Validated.map
|
||||||
(Comp.Dropdown.getSelected model.langModel
|
(\cls ->
|
||||||
|> List.head
|
{ language =
|
||||||
|> Maybe.map Data.Language.toIso3
|
Comp.Dropdown.getSelected model.langModel
|
||||||
|> Maybe.withDefault model.initSettings.language
|
|> List.head
|
||||||
|
|> Maybe.map Data.Language.toIso3
|
||||||
|
|> Maybe.withDefault model.initSettings.language
|
||||||
|
, integrationEnabled = model.intEnabled
|
||||||
|
, classifier = cls
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(Comp.ClassifierSettingsForm.getSettings
|
||||||
|
model.classifierModel
|
||||||
)
|
)
|
||||||
model.intEnabled
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
@ -73,6 +89,8 @@ type Msg
|
|||||||
| SetFullTextConfirm String
|
| SetFullTextConfirm String
|
||||||
| TriggerReIndex
|
| TriggerReIndex
|
||||||
| TriggerReIndexResult (Result Http.Error BasicResult)
|
| TriggerReIndexResult (Result Http.Error BasicResult)
|
||||||
|
| ClassifierSettingMsg Comp.ClassifierSettingsForm.Msg
|
||||||
|
| SaveSettings
|
||||||
|
|
||||||
|
|
||||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe CollectiveSettings )
|
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe CollectiveSettings )
|
||||||
@ -85,22 +103,15 @@ update flags msg model =
|
|||||||
|
|
||||||
nextModel =
|
nextModel =
|
||||||
{ model | langModel = m2 }
|
{ model | langModel = m2 }
|
||||||
|
|
||||||
nextSettings =
|
|
||||||
if Comp.Dropdown.isDropdownChangeMsg m then
|
|
||||||
Just (getSettings nextModel)
|
|
||||||
|
|
||||||
else
|
|
||||||
Nothing
|
|
||||||
in
|
in
|
||||||
( nextModel, Cmd.map LangDropdownMsg c2, nextSettings )
|
( nextModel, Cmd.map LangDropdownMsg c2, Nothing )
|
||||||
|
|
||||||
ToggleIntegrationEndpoint ->
|
ToggleIntegrationEndpoint ->
|
||||||
let
|
let
|
||||||
nextModel =
|
nextModel =
|
||||||
{ model | intEnabled = not model.intEnabled }
|
{ model | intEnabled = not model.intEnabled }
|
||||||
in
|
in
|
||||||
( nextModel, Cmd.none, Just (getSettings nextModel) )
|
( nextModel, Cmd.none, Nothing )
|
||||||
|
|
||||||
SetFullTextConfirm str ->
|
SetFullTextConfirm str ->
|
||||||
( { model | fullTextConfirmText = str }, Cmd.none, Nothing )
|
( { model | fullTextConfirmText = str }, Cmd.none, Nothing )
|
||||||
@ -138,6 +149,26 @@ update flags msg model =
|
|||||||
, Nothing
|
, Nothing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ClassifierSettingMsg lmsg ->
|
||||||
|
let
|
||||||
|
( cm, cc ) =
|
||||||
|
Comp.ClassifierSettingsForm.update flags lmsg model.classifierModel
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| classifierModel = cm
|
||||||
|
}
|
||||||
|
, Cmd.map ClassifierSettingMsg cc
|
||||||
|
, Nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
SaveSettings ->
|
||||||
|
case getSettings model of
|
||||||
|
Data.Validated.Valid s ->
|
||||||
|
( model, Cmd.none, Just s )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Cmd.none, Nothing )
|
||||||
|
|
||||||
|
|
||||||
view : Flags -> UiSettings -> Model -> Html Msg
|
view : Flags -> UiSettings -> Model -> Html Msg
|
||||||
view flags settings model =
|
view flags settings model =
|
||||||
@ -232,4 +263,31 @@ view flags settings model =
|
|||||||
|> text
|
|> text
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
, h3
|
||||||
|
[ classList
|
||||||
|
[ ( "ui dividing header", True )
|
||||||
|
, ( "invisible hidden", False )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ text "Document Classifier"
|
||||||
|
]
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "field", True )
|
||||||
|
, ( "invisible hidden", False )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Html.map ClassifierSettingMsg
|
||||||
|
(Comp.ClassifierSettingsForm.view model.classifierModel)
|
||||||
|
]
|
||||||
|
, div [ class "ui divider" ] []
|
||||||
|
, button
|
||||||
|
[ classList
|
||||||
|
[ ( "ui primary button", True )
|
||||||
|
, ( "disabled", getSettings model |> Data.Validated.isInvalid )
|
||||||
|
]
|
||||||
|
, onClick SaveSettings
|
||||||
|
]
|
||||||
|
[ text "Save"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
module Data.Validated exposing
|
module Data.Validated exposing
|
||||||
( Validated(..)
|
( Validated(..)
|
||||||
|
, isInvalid
|
||||||
, map
|
, map
|
||||||
, map2
|
, map2
|
||||||
, map3
|
, map3
|
||||||
@ -14,6 +15,19 @@ type Validated a
|
|||||||
| Unknown a
|
| Unknown a
|
||||||
|
|
||||||
|
|
||||||
|
isInvalid : Validated a -> Bool
|
||||||
|
isInvalid v =
|
||||||
|
case v of
|
||||||
|
Valid _ ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Invalid _ _ ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Unknown _ ->
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
value : Validated a -> a
|
value : Validated a -> a
|
||||||
value va =
|
value va =
|
||||||
case va of
|
case va of
|
||||||
|
@ -30,15 +30,21 @@ init flags =
|
|||||||
let
|
let
|
||||||
( sm, sc ) =
|
( sm, sc ) =
|
||||||
Comp.SourceManage.init flags
|
Comp.SourceManage.init flags
|
||||||
|
|
||||||
|
( cm, cc ) =
|
||||||
|
Comp.CollectiveSettingsForm.init flags Api.Model.CollectiveSettings.empty
|
||||||
in
|
in
|
||||||
( { currentTab = Just InsightsTab
|
( { currentTab = Just InsightsTab
|
||||||
, sourceModel = sm
|
, sourceModel = sm
|
||||||
, userModel = Comp.UserManage.emptyModel
|
, userModel = Comp.UserManage.emptyModel
|
||||||
, settingsModel = Comp.CollectiveSettingsForm.init Api.Model.CollectiveSettings.empty
|
, settingsModel = cm
|
||||||
, insights = Api.Model.ItemInsights.empty
|
, insights = Api.Model.ItemInsights.empty
|
||||||
, submitResult = Nothing
|
, submitResult = Nothing
|
||||||
}
|
}
|
||||||
, Cmd.map SourceMsg sc
|
, Cmd.batch
|
||||||
|
[ Cmd.map SourceMsg sc
|
||||||
|
, Cmd.map SettingsFormMsg cc
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +77,13 @@ update flags msg model =
|
|||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
CollectiveSettingsResp (Ok data) ->
|
CollectiveSettingsResp (Ok data) ->
|
||||||
( { model | settingsModel = Comp.CollectiveSettingsForm.init data }, Cmd.none )
|
let
|
||||||
|
( cm, cc ) =
|
||||||
|
Comp.CollectiveSettingsForm.init flags data
|
||||||
|
in
|
||||||
|
( { model | settingsModel = cm }
|
||||||
|
, Cmd.map SettingsFormMsg cc
|
||||||
|
)
|
||||||
|
|
||||||
CollectiveSettingsResp (Err _) ->
|
CollectiveSettingsResp (Err _) ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
@ -185,10 +185,11 @@ viewSettings : Flags -> UiSettings -> Model -> List (Html Msg)
|
|||||||
viewSettings flags settings model =
|
viewSettings flags settings model =
|
||||||
[ h2 [ class "ui header" ]
|
[ h2 [ class "ui header" ]
|
||||||
[ i [ class "cog icon" ] []
|
[ i [ class "cog icon" ] []
|
||||||
, text "Settings"
|
, text "Collective Settings"
|
||||||
]
|
]
|
||||||
, div [ class "ui segment" ]
|
, div [ class "ui segment" ]
|
||||||
[ Html.map SettingsFormMsg (Comp.CollectiveSettingsForm.view flags settings model.settingsModel)
|
[ Html.map SettingsFormMsg
|
||||||
|
(Comp.CollectiveSettingsForm.view flags settings model.settingsModel)
|
||||||
]
|
]
|
||||||
, div
|
, div
|
||||||
[ classList
|
[ classList
|
||||||
|
Loading…
x
Reference in New Issue
Block a user